ADD new frontend
This commit is contained in:
686
frontend/src/components/ui/Table/Table.tsx
Normal file
686
frontend/src/components/ui/Table/Table.tsx
Normal file
@@ -0,0 +1,686 @@
|
||||
import React, { forwardRef, useMemo, useState, useEffect, TableHTMLAttributes } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface TableColumn<T = any> {
|
||||
key: string;
|
||||
title: string;
|
||||
dataIndex?: keyof T;
|
||||
render?: (value: any, record: T, index: number) => React.ReactNode;
|
||||
width?: string | number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
sortable?: boolean;
|
||||
sortKey?: string;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
fixed?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export interface TableProps<T = any> extends Omit<TableHTMLAttributes<HTMLTableElement>, 'onSelect'> {
|
||||
columns: TableColumn<T>[];
|
||||
data: T[];
|
||||
loading?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'default' | 'striped' | 'bordered' | 'borderless';
|
||||
hover?: boolean;
|
||||
sticky?: boolean;
|
||||
rowSelection?: {
|
||||
type?: 'checkbox' | 'radio';
|
||||
selectedRowKeys?: React.Key[];
|
||||
onSelect?: (record: T, selected: boolean, selectedRows: T[], nativeEvent: Event) => void;
|
||||
onSelectAll?: (selected: boolean, selectedRows: T[], changeRows: T[]) => void;
|
||||
onSelectInvert?: (selectedRowKeys: React.Key[]) => void;
|
||||
getCheckboxProps?: (record: T) => { disabled?: boolean; name?: string };
|
||||
hideSelectAll?: boolean;
|
||||
preserveSelectedRowKeys?: boolean;
|
||||
};
|
||||
pagination?: {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
total?: number;
|
||||
showSizeChanger?: boolean;
|
||||
showQuickJumper?: boolean;
|
||||
showTotal?: (total: number, range: [number, number]) => string;
|
||||
onChange?: (page: number, pageSize: number) => void;
|
||||
position?: 'top' | 'bottom' | 'both';
|
||||
};
|
||||
sort?: {
|
||||
field?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
multiple?: boolean;
|
||||
};
|
||||
onSort?: (field: string, order: 'asc' | 'desc' | null) => void;
|
||||
expandable?: {
|
||||
expandedRowKeys?: React.Key[];
|
||||
onExpand?: (expanded: boolean, record: T) => void;
|
||||
onExpandedRowsChange?: (expandedKeys: React.Key[]) => void;
|
||||
expandedRowRender?: (record: T, index: number) => React.ReactNode;
|
||||
rowExpandable?: (record: T) => boolean;
|
||||
expandRowByClick?: boolean;
|
||||
defaultExpandAllRows?: boolean;
|
||||
indentSize?: number;
|
||||
};
|
||||
rowKey?: string | ((record: T) => React.Key);
|
||||
onRow?: (record: T, index: number) => React.HTMLAttributes<HTMLTableRowElement>;
|
||||
locale?: {
|
||||
emptyText?: string;
|
||||
selectAll?: string;
|
||||
selectRow?: string;
|
||||
expand?: string;
|
||||
collapse?: string;
|
||||
};
|
||||
scroll?: {
|
||||
x?: string | number | true;
|
||||
y?: string | number;
|
||||
};
|
||||
summary?: (data: T[]) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface TablePaginationProps {
|
||||
current: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
showSizeChanger?: boolean;
|
||||
showQuickJumper?: boolean;
|
||||
showTotal?: (total: number, range: [number, number]) => string;
|
||||
onChange?: (page: number, pageSize: number) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Table = forwardRef<HTMLTableElement, TableProps>(({
|
||||
columns,
|
||||
data,
|
||||
loading = false,
|
||||
size = 'md',
|
||||
variant = 'default',
|
||||
hover = true,
|
||||
sticky = false,
|
||||
rowSelection,
|
||||
pagination,
|
||||
sort,
|
||||
onSort,
|
||||
expandable,
|
||||
rowKey = 'id',
|
||||
onRow,
|
||||
locale = {
|
||||
emptyText: 'No hay datos disponibles',
|
||||
selectAll: 'Seleccionar todo',
|
||||
selectRow: 'Seleccionar fila',
|
||||
expand: 'Expandir fila',
|
||||
collapse: 'Contraer fila',
|
||||
},
|
||||
scroll,
|
||||
summary,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const [sortState, setSortState] = useState<{ field?: string; order?: 'asc' | 'desc' }>({
|
||||
field: sort?.field,
|
||||
order: sort?.order,
|
||||
});
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>(
|
||||
rowSelection?.selectedRowKeys || []
|
||||
);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>(
|
||||
expandable?.expandedRowKeys || (expandable?.defaultExpandAllRows ? data.map(getRowKey) : [])
|
||||
);
|
||||
|
||||
function getRowKey(record: any, index?: number): React.Key {
|
||||
if (typeof rowKey === 'function') {
|
||||
return rowKey(record);
|
||||
}
|
||||
return record[rowKey] || index || 0;
|
||||
}
|
||||
|
||||
// Update local state when props change
|
||||
useEffect(() => {
|
||||
if (rowSelection?.selectedRowKeys) {
|
||||
setSelectedRowKeys(rowSelection.selectedRowKeys);
|
||||
}
|
||||
}, [rowSelection?.selectedRowKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
if (expandable?.expandedRowKeys) {
|
||||
setExpandedRowKeys(expandable.expandedRowKeys);
|
||||
}
|
||||
}, [expandable?.expandedRowKeys]);
|
||||
|
||||
// Handle sorting
|
||||
const handleSort = (field: string) => {
|
||||
let newOrder: 'asc' | 'desc' | null = 'asc';
|
||||
|
||||
if (sortState.field === field) {
|
||||
newOrder = sortState.order === 'asc' ? 'desc' : sortState.order === 'desc' ? null : 'asc';
|
||||
}
|
||||
|
||||
setSortState({ field: newOrder ? field : undefined, order: newOrder || undefined });
|
||||
onSort?.(field, newOrder);
|
||||
};
|
||||
|
||||
// Handle row selection
|
||||
const handleSelectRow = (record: any, selected: boolean, event: Event) => {
|
||||
const key = getRowKey(record);
|
||||
let newSelectedKeys = [...selectedRowKeys];
|
||||
|
||||
if (rowSelection?.type === 'radio') {
|
||||
newSelectedKeys = selected ? [key] : [];
|
||||
} else {
|
||||
if (selected) {
|
||||
newSelectedKeys.push(key);
|
||||
} else {
|
||||
newSelectedKeys = newSelectedKeys.filter(k => k !== key);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedRowKeys(newSelectedKeys);
|
||||
const selectedRows = data.filter(item => newSelectedKeys.includes(getRowKey(item)));
|
||||
rowSelection?.onSelect?.(record, selected, selectedRows, event);
|
||||
};
|
||||
|
||||
const handleSelectAll = (selected: boolean) => {
|
||||
const selectableData = data.filter(record => {
|
||||
const checkboxProps = rowSelection?.getCheckboxProps?.(record);
|
||||
return !checkboxProps?.disabled;
|
||||
});
|
||||
|
||||
const newSelectedKeys = selected ? selectableData.map(getRowKey) : [];
|
||||
const changeRows = selected ? selectableData : selectedRowKeys.map(key => data.find(item => getRowKey(item) === key)).filter(Boolean);
|
||||
|
||||
setSelectedRowKeys(newSelectedKeys);
|
||||
const selectedRows = data.filter(item => newSelectedKeys.includes(getRowKey(item)));
|
||||
rowSelection?.onSelectAll?.(selected, selectedRows, changeRows);
|
||||
};
|
||||
|
||||
// Handle row expansion
|
||||
const handleExpand = (record: any, expanded: boolean) => {
|
||||
const key = getRowKey(record);
|
||||
let newExpandedKeys = [...expandedRowKeys];
|
||||
|
||||
if (expanded) {
|
||||
newExpandedKeys.push(key);
|
||||
} else {
|
||||
newExpandedKeys = newExpandedKeys.filter(k => k !== key);
|
||||
}
|
||||
|
||||
setExpandedRowKeys(newExpandedKeys);
|
||||
expandable?.onExpand?.(expanded, record);
|
||||
expandable?.onExpandedRowsChange?.(newExpandedKeys);
|
||||
};
|
||||
|
||||
// Processed data for current page
|
||||
const processedData = useMemo(() => {
|
||||
let result = [...data];
|
||||
|
||||
// Apply pagination if needed
|
||||
if (pagination) {
|
||||
const { current = 1, pageSize = 10 } = pagination;
|
||||
const start = (current - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
result = result.slice(start, end);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [data, pagination]);
|
||||
|
||||
// Table classes
|
||||
const sizeClasses = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-base',
|
||||
};
|
||||
|
||||
const variantClasses = {
|
||||
default: 'border-collapse border border-table-border',
|
||||
striped: 'border-collapse',
|
||||
bordered: 'border-collapse border border-table-border',
|
||||
borderless: 'border-collapse',
|
||||
};
|
||||
|
||||
const tableClasses = clsx(
|
||||
'w-full bg-table-bg',
|
||||
sizeClasses[size],
|
||||
variantClasses[variant],
|
||||
{
|
||||
'table-fixed': scroll?.x,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
const containerClasses = clsx(
|
||||
'overflow-auto',
|
||||
{
|
||||
'max-h-96': scroll?.y,
|
||||
}
|
||||
);
|
||||
|
||||
// Render loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-color-primary"></div>
|
||||
<span className="text-text-secondary">Cargando...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render empty state
|
||||
if (processedData.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<div className="text-text-tertiary mb-2">
|
||||
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-text-secondary">{locale.emptyText}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderHeader = () => {
|
||||
const hasSelection = rowSelection && !rowSelection.hideSelectAll;
|
||||
const hasExpansion = expandable;
|
||||
|
||||
return (
|
||||
<thead className={clsx('bg-table-header-bg', { 'sticky top-0 z-10': sticky })}>
|
||||
<tr>
|
||||
{hasSelection && (
|
||||
<th className="px-4 py-3 text-left font-medium text-text-primary border-b border-table-border w-12">
|
||||
{rowSelection.type !== 'radio' && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-input-border focus:ring-color-primary"
|
||||
checked={selectedRowKeys.length === data.length && data.length > 0}
|
||||
indeterminate={selectedRowKeys.length > 0 && selectedRowKeys.length < data.length}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
aria-label={locale.selectAll}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
)}
|
||||
{hasExpansion && (
|
||||
<th className="px-4 py-3 text-left font-medium text-text-primary border-b border-table-border w-12"></th>
|
||||
)}
|
||||
{columns.map((column) => {
|
||||
const isSorted = sortState.field === (column.sortKey || column.key);
|
||||
|
||||
return (
|
||||
<th
|
||||
key={column.key}
|
||||
className={clsx(
|
||||
'px-4 py-3 font-medium text-text-primary border-b border-table-border',
|
||||
{
|
||||
'text-left': column.align === 'left' || !column.align,
|
||||
'text-center': column.align === 'center',
|
||||
'text-right': column.align === 'right',
|
||||
'cursor-pointer hover:bg-table-row-hover': column.sortable,
|
||||
},
|
||||
column.headerClassName
|
||||
)}
|
||||
style={{ width: column.width }}
|
||||
onClick={column.sortable ? () => handleSort(column.sortKey || column.key) : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{column.title}</span>
|
||||
{column.sortable && (
|
||||
<span className="flex flex-col">
|
||||
<svg
|
||||
className={clsx('w-3 h-3', {
|
||||
'text-color-primary': isSorted && sortState.order === 'asc',
|
||||
'text-text-quaternary': !isSorted || sortState.order !== 'asc',
|
||||
})}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" />
|
||||
</svg>
|
||||
<svg
|
||||
className={clsx('w-3 h-3 -mt-1', {
|
||||
'text-color-primary': isSorted && sortState.order === 'desc',
|
||||
'text-text-quaternary': !isSorted || sortState.order !== 'desc',
|
||||
})}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBody = () => (
|
||||
<tbody>
|
||||
{processedData.map((record, index) => {
|
||||
const key = getRowKey(record, index);
|
||||
const isSelected = selectedRowKeys.includes(key);
|
||||
const isExpanded = expandedRowKeys.includes(key);
|
||||
const canExpand = expandable?.rowExpandable?.(record) !== false;
|
||||
const rowProps = onRow?.(record, index) || {};
|
||||
|
||||
return (
|
||||
<React.Fragment key={key}>
|
||||
<tr
|
||||
{...rowProps}
|
||||
className={clsx(
|
||||
'transition-colors duration-150',
|
||||
{
|
||||
'bg-table-row-hover': hover && !isSelected,
|
||||
'bg-table-row-selected': isSelected,
|
||||
'odd:bg-bg-secondary': variant === 'striped' && !isSelected,
|
||||
'border-b border-table-border': variant !== 'borderless',
|
||||
'cursor-pointer': expandable?.expandRowByClick,
|
||||
},
|
||||
rowProps.className
|
||||
)}
|
||||
onClick={expandable?.expandRowByClick ? () => handleExpand(record, !isExpanded) : rowProps.onClick}
|
||||
>
|
||||
{rowSelection && (
|
||||
<td className="px-4 py-3 border-b border-table-border">
|
||||
<input
|
||||
type={rowSelection.type || 'checkbox'}
|
||||
className="rounded border-input-border focus:ring-color-primary"
|
||||
checked={isSelected}
|
||||
onChange={(e) => handleSelectRow(record, e.target.checked, e.nativeEvent)}
|
||||
disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
|
||||
name={rowSelection.getCheckboxProps?.(record)?.name}
|
||||
aria-label={`${locale.selectRow} ${index + 1}`}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{expandable && (
|
||||
<td className="px-4 py-3 border-b border-table-border">
|
||||
{canExpand && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-tertiary hover:text-text-primary transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-color-primary/20 rounded p-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleExpand(record, !isExpanded);
|
||||
}}
|
||||
aria-label={isExpanded ? locale.collapse : locale.expand}
|
||||
>
|
||||
<svg
|
||||
className={clsx('w-4 h-4 transform transition-transform duration-150', {
|
||||
'rotate-90': isExpanded,
|
||||
})}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{columns.map((column) => {
|
||||
const value = column.dataIndex ? record[column.dataIndex] : record;
|
||||
const content = column.render ? column.render(value, record, index) : value;
|
||||
|
||||
return (
|
||||
<td
|
||||
key={column.key}
|
||||
className={clsx(
|
||||
'px-4 py-3 border-b border-table-border',
|
||||
{
|
||||
'text-left': column.align === 'left' || !column.align,
|
||||
'text-center': column.align === 'center',
|
||||
'text-right': column.align === 'right',
|
||||
},
|
||||
column.className
|
||||
)}
|
||||
style={{ width: column.width }}
|
||||
>
|
||||
{content}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
{isExpanded && expandable?.expandedRowRender && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length + (rowSelection ? 1 : 0) + (expandable ? 1 : 0)}
|
||||
className="px-4 py-0 border-b border-table-border bg-bg-secondary"
|
||||
>
|
||||
<div className="py-4" style={{ paddingLeft: expandable.indentSize || 24 }}>
|
||||
{expandable.expandedRowRender(record, index)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{summary && (
|
||||
<tr className="bg-table-header-bg border-t-2 border-table-border font-medium">
|
||||
<td colSpan={columns.length + (rowSelection ? 1 : 0) + (expandable ? 1 : 0)}>
|
||||
{summary(data)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className={containerClasses}>
|
||||
<table
|
||||
ref={ref}
|
||||
className={tableClasses}
|
||||
{...props}
|
||||
>
|
||||
{renderHeader()}
|
||||
{renderBody()}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<TablePagination
|
||||
current={pagination.current || 1}
|
||||
pageSize={pagination.pageSize || 10}
|
||||
total={pagination.total || data.length}
|
||||
showSizeChanger={pagination.showSizeChanger}
|
||||
showQuickJumper={pagination.showQuickJumper}
|
||||
showTotal={pagination.showTotal}
|
||||
onChange={pagination.onChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const TablePagination: React.FC<TablePaginationProps> = ({
|
||||
current,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger = false,
|
||||
showQuickJumper = false,
|
||||
showTotal,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const startRecord = (current - 1) * pageSize + 1;
|
||||
const endRecord = Math.min(current * pageSize, total);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages && page !== current) {
|
||||
onChange?.(page, pageSize);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = (newSize: number) => {
|
||||
const newTotalPages = Math.ceil(total / newSize);
|
||||
const newPage = current > newTotalPages ? newTotalPages : current;
|
||||
onChange?.(newPage || 1, newSize);
|
||||
};
|
||||
|
||||
const renderPageButtons = () => {
|
||||
const buttons = [];
|
||||
const maxVisible = 7;
|
||||
let startPage = Math.max(1, current - Math.floor(maxVisible / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
|
||||
|
||||
if (endPage - startPage + 1 < maxVisible) {
|
||||
startPage = Math.max(1, endPage - maxVisible + 1);
|
||||
}
|
||||
|
||||
// Previous button
|
||||
buttons.push(
|
||||
<button
|
||||
key="prev"
|
||||
type="button"
|
||||
className="px-3 py-2 text-sm font-medium text-text-secondary border border-border-primary rounded-l-md hover:bg-bg-secondary disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150"
|
||||
disabled={current === 1}
|
||||
onClick={() => handlePageChange(current - 1)}
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
);
|
||||
|
||||
// First page and ellipsis
|
||||
if (startPage > 1) {
|
||||
buttons.push(
|
||||
<button
|
||||
key={1}
|
||||
type="button"
|
||||
className="px-3 py-2 text-sm font-medium text-text-secondary border-t border-b border-border-primary hover:bg-bg-secondary transition-colors duration-150"
|
||||
onClick={() => handlePageChange(1)}
|
||||
>
|
||||
1
|
||||
</button>
|
||||
);
|
||||
|
||||
if (startPage > 2) {
|
||||
buttons.push(
|
||||
<span key="start-ellipsis" className="px-3 py-2 text-sm text-text-tertiary border-t border-b border-border-primary">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Page number buttons
|
||||
for (let page = startPage; page <= endPage; page++) {
|
||||
buttons.push(
|
||||
<button
|
||||
key={page}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'px-3 py-2 text-sm font-medium border-t border-b border-border-primary transition-colors duration-150',
|
||||
{
|
||||
'bg-color-primary text-text-inverse': page === current,
|
||||
'text-text-secondary hover:bg-bg-secondary': page !== current,
|
||||
}
|
||||
)}
|
||||
onClick={() => handlePageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Last page and ellipsis
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
buttons.push(
|
||||
<span key="end-ellipsis" className="px-3 py-2 text-sm text-text-tertiary border-t border-b border-border-primary">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<button
|
||||
key={totalPages}
|
||||
type="button"
|
||||
className="px-3 py-2 text-sm font-medium text-text-secondary border-t border-b border-border-primary hover:bg-bg-secondary transition-colors duration-150"
|
||||
onClick={() => handlePageChange(totalPages)}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Next button
|
||||
buttons.push(
|
||||
<button
|
||||
key="next"
|
||||
type="button"
|
||||
className="px-3 py-2 text-sm font-medium text-text-secondary border border-border-primary rounded-r-md hover:bg-bg-secondary disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150"
|
||||
disabled={current === totalPages}
|
||||
onClick={() => handlePageChange(current + 1)}
|
||||
>
|
||||
Siguiente
|
||||
</button>
|
||||
);
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-center justify-between', className)}>
|
||||
<div className="flex items-center gap-4">
|
||||
{showTotal && (
|
||||
<span className="text-sm text-text-secondary">
|
||||
{showTotal(total, [startRecord, endRecord])}
|
||||
</span>
|
||||
)}
|
||||
{showSizeChanger && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-text-secondary">Mostrar</span>
|
||||
<select
|
||||
className="px-2 py-1 text-sm border border-border-primary rounded-md bg-input-bg focus:outline-none focus:ring-2 focus:ring-color-primary/20"
|
||||
value={pageSize}
|
||||
onChange={(e) => handleSizeChange(Number(e.target.value))}
|
||||
>
|
||||
{[10, 20, 50, 100].map(size => (
|
||||
<option key={size} value={size}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-sm text-text-secondary">por página</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{showQuickJumper && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-text-secondary">Ir a</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
className="w-16 px-2 py-1 text-sm text-center border border-border-primary rounded-md bg-input-bg focus:outline-none focus:ring-2 focus:ring-color-primary/20"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const page = Number((e.target as HTMLInputElement).value);
|
||||
handlePageChange(page);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
{renderPageButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Table.displayName = 'Table';
|
||||
|
||||
export default Table;
|
||||
export { TablePagination };
|
||||
Reference in New Issue
Block a user