import { useState, type HTMLAttributes, type MouseEvent, type ReactNode } from 'react'; import { ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react'; import { Pagination } from '../Pagination/Pagination'; import styles from './Table.module.css'; export type SortDirection = 'asc' | 'desc'; export interface TableColumn { key: string; header: ReactNode; render: (row: T) => ReactNode; width?: string | number; align?: 'left' | 'center' | 'right'; /** If true, a sort icon is shown and clicking the header triggers onSort */ sortable?: boolean; } export interface TableProps { columns: TableColumn[]; data: T[]; rowKey: (row: T) => string | number; loading?: boolean; emptyMessage?: string; className?: string; /** * Client-side pagination: pass a default page size to enable. * For server-side pagination use `total` + `page` + `onPageChange` instead. */ pageSize?: number; /** Offer a page-size selector when pagination is enabled */ pageSizeOptions?: number[]; /** Called when a row is clicked */ onRowClick?: (row: T) => void; /** Optional per-row props, useful for drag-and-drop and row-level attributes */ getRowProps?: (row: T) => HTMLAttributes; /** Currently sorted column key (controlled) */ sortBy?: string; /** Current sort direction (controlled) */ sortDir?: SortDirection; /** Called when the user clicks a sortable column header */ onSort?: (key: string, dir: SortDirection) => void; // ── Server-side pagination (controlled) ────────────────────────────────── /** Total record count from server. Presence switches to server-side pagination mode. */ total?: number; /** Controlled current page (1-based). Required when `total` is set. */ page?: number; /** Called when the user navigates to a new page. Required when `total` is set. */ onPageChange?: (page: number) => void; /** Called when the user changes page size. Optional for server-side pagination. */ onPageSizeChange?: (size: number) => void; } export function Table({ columns, data, rowKey, loading = false, emptyMessage = 'No data.', className, pageSize: defaultPageSize, pageSizeOptions, onRowClick, getRowProps, sortBy, sortDir, onSort, total, page: controlledPage, onPageChange, onPageSizeChange, }: TableProps) { const [internalPage, setInternalPage] = useState(1); const [pageSize, setPageSize] = useState(defaultPageSize ?? 10); const serverMode = total !== undefined; const handleSortClick = (key: string) => { if (!onSort) return; const nextDir: SortDirection = sortBy === key && sortDir === 'asc' ? 'desc' : 'asc'; onSort(key, nextDir); }; // Client-side pagination const clientPaginated = !serverMode && defaultPageSize !== undefined && !loading; const visibleData = clientPaginated ? data.slice((internalPage - 1) * pageSize, internalPage * pageSize) : data; const lastClientPage = pageSize > 0 ? Math.ceil(data.length / pageSize) : 1; const safeInternalPage = Math.min(internalPage, Math.max(1, lastClientPage)); // Server-side pagination display values const serverPage = controlledPage ?? 1; const serverTotal = total ?? 0; return (
{columns.map((col) => { const isSorted = col.sortable && sortBy === col.key; const SortIcon = isSorted ? sortDir === 'asc' ? ArrowUp : ArrowDown : ArrowUpDown; return ( ); })} {loading ? ( ) : visibleData.length === 0 ? ( ) : ( visibleData.map((row) => { const rowProps = getRowProps?.(row); const mergedClassName = [ styles.tr, onRowClick ? styles.clickable : '', rowProps?.className ?? '', ] .filter(Boolean) .join(' '); const handleClick = (event: MouseEvent) => { rowProps?.onClick?.(event); if (!event.defaultPrevented) { onRowClick?.(row); } }; return ( {columns.map((col) => ( ))} ); }) )}
handleSortClick(col.key) : undefined} > {col.header} {col.sortable && (
Loading…
{emptyMessage}
e.stopPropagation() : undefined} > {col.render(row)}
{serverMode && serverTotal > 0 && onPageChange && ( { setPageSize(size); onPageSizeChange(size); } : pageSizeOptions ? (size) => { setPageSize(size); onPageChange(1); } : undefined } pageSizeOptions={pageSizeOptions} /> )} {clientPaginated && data.length > 0 && ( { setPageSize(size); setInternalPage(1); } : undefined } pageSizeOptions={pageSizeOptions} /> )}
); }