import React, { forwardRef, useState, useMemo } from "react"; import { BaseComponentProps } from "../../types"; import { Button } from "../ui/Button"; import { Input } from "../ui/Input"; export interface TableColumn> { key: string; label: string; sortable?: boolean; filterable?: boolean; render?: (value: unknown, row: T) => React.ReactNode; width?: string; } export interface DataTableProps> extends BaseComponentProps { data: T[]; columns: TableColumn[]; loading?: boolean; pagination?: { page: number; pageSize: number; total: number; onPageChange: (page: number) => void; onPageSizeChange: (pageSize: number) => void; }; searchable?: boolean; onSearch?: (query: string) => void; emptyMessage?: string; variant?: "default" | "striped"; } export const DataTable = forwardRef( ( { className = "", data, columns, loading = false, pagination, searchable = false, onSearch, emptyMessage = "No data available", variant = "default", ...props }, ref ) => { const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc"; } | null>(null); const [searchQuery, setSearchQuery] = useState(""); const sortedData = useMemo(() => { if (!sortConfig) return data; return [...data].sort((a, b) => { const aValue = (a as Record)[sortConfig.key]; const bValue = (b as Record)[sortConfig.key]; // Safe comparison for unknown types const aStr = aValue?.toString() ?? ""; const bStr = bValue?.toString() ?? ""; if (aStr < bStr) { return sortConfig.direction === "asc" ? -1 : 1; } if (aStr > bStr) { return sortConfig.direction === "asc" ? 1 : -1; } return 0; }); }, [data, sortConfig]); const handleSort = (columnKey: string) => { const column = columns.find((col) => col.key === columnKey); if (!column?.sortable) return; setSortConfig((current) => { if (current?.key === columnKey) { return current.direction === "asc" ? { key: columnKey, direction: "desc" } : null; } return { key: columnKey, direction: "asc" }; }); }; const handleSearch = (query: string) => { setSearchQuery(query); onSearch?.(query); }; const getSortIcon = (columnKey: string) => { if (sortConfig?.key !== columnKey) { return ( ); } return sortConfig.direction === "asc" ? ( ) : ( ); }; const renderLoadingSkeleton = () => (
{Array.from({ length: 5 }).map((_, index) => ( {columns.map((column) => (
))} ))}
); return (
{/* Search */} {searchable && (
handleSearch(e.target.value)} className="max-w-sm" />
)} {/* Table */}
{columns.map((column) => ( ))} {loading && renderLoadingSkeleton()} {!loading && sortedData.length === 0 && ( )} {!loading && sortedData.map((row, rowIndex) => ( {columns.map((column) => ( ))} ))}
handleSort(column.key)} >
{column.label} {column.sortable && getSortIcon(column.key)}
{emptyMessage}
{column.render ? column.render( (row as Record)[column.key], row ) : String( (row as Record)[column.key] ?? "" )}
{/* Pagination */} {pagination && (
Showing {(pagination.page - 1) * pagination.pageSize + 1} to{" "} {Math.min( pagination.page * pagination.pageSize, pagination.total )}{" "} of {pagination.total} results
Page {pagination.page} of{" "} {Math.ceil(pagination.total / pagination.pageSize)}
)}
); } ); DataTable.displayName = "DataTable";