import { useEffect, useMemo, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./table"; import { Button } from "./button"; import { Input } from "./input"; export type ColumnDef = { id: string; header: string; accessor: (row: T) => React.ReactNode; sortable?: boolean; filterable?: boolean; sortFn?: (a: T, b: T) => number; filterFn?: (row: T, filterValue: string) => boolean; }; export type SortConfig = { columnId: string; direction: "asc" | "desc"; } | null; export type DataTableProps = { columns: ColumnDef[]; data: T[]; pageSize?: number; initialSort?: SortConfig; getRowKey: (row: T, index: number) => string; emptyMessage?: string; }; export function DataTable({ columns, data, pageSize = 10, initialSort = null, getRowKey, emptyMessage = "No data available", }: DataTableProps) { const [currentPage, setCurrentPage] = useState(1); const [sortConfig, setSortConfig] = useState(initialSort); const [filters, setFilters] = useState>({}); // Apply filtering const filteredData = useMemo(() => { return data.filter((row) => { return Object.entries(filters).every(([columnId, filterValue]) => { if (!filterValue) return true; const column = columns.find((col) => col.id === columnId); if (!column?.filterable) return true; if (column.filterFn) { return column.filterFn(row, filterValue); } // Default filter: convert to string and check inclusion const cellValue = String(column.accessor(row)); return cellValue.toLowerCase().includes(filterValue.toLowerCase()); }); }); }, [data, filters, columns]); // Apply sorting const sortedData = useMemo(() => { if (!sortConfig) return filteredData; const column = columns.find((col) => col.id === sortConfig.columnId); if (!column?.sortable) return filteredData; const sorted = [...filteredData].sort((a, b) => { if (column.sortFn) { return column.sortFn(a, b); } // Default sort: compare string values const aValue = String(column.accessor(a)); const bValue = String(column.accessor(b)); return aValue.localeCompare(bValue); }); return sortConfig.direction === "desc" ? sorted.reverse() : sorted; }, [filteredData, sortConfig, columns]); // Calculate pagination const totalPages = Math.ceil(sortedData.length / pageSize); const showPagination = sortedData.length > pageSize; const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedData = sortedData.slice(startIndex, endIndex); // Reset to page 1 when filters change useEffect(() => { setCurrentPage(1); }, [filters]); const handleSort = (columnId: string) => { const column = columns.find((col) => col.id === columnId); if (!column?.sortable) return; setSortConfig((current) => { if (current?.columnId === columnId) { if (current.direction === "asc") { return { columnId, direction: "desc" }; } return null; // Remove sorting } return { columnId, direction: "asc" }; }); }; const handleFilterChange = (columnId: string, value: string) => { setFilters((current) => ({ ...current, [columnId]: value, })); }; const handlePageChange = (page: number) => { setCurrentPage(Math.max(1, Math.min(page, totalPages))); }; return ( <> {columns.map((column) => (
handleSort(column.id)} > {column.header} {column.sortable && ( {sortConfig?.columnId === column.id ? sortConfig.direction === "asc" ? "↑" : "↓" : "↕"} )}
))}
{columns.some((column) => column.filterable) && ( {columns.map((column) => ( {column.filterable && ( handleFilterChange(column.id, e.target.value) } onClick={(e) => e.stopPropagation()} /> )} ))} )} {paginatedData.length === 0 ? (
{emptyMessage}
) : ( paginatedData.map((row, index) => ( {columns.map((column) => ( {column.accessor(row)} ))} )) )}
{showPagination && (
Showing {startIndex + 1} to {Math.min(endIndex, sortedData.length)}{" "} of {sortedData.length} entries {Object.keys(filters).some((key) => filters[key]) && ` (filtered from ${data.length})`}
Page {currentPage} of {totalPages}
)} ); }