interface Column { key: string; label: string; render: (item: T) => React.ReactNode; className?: string; } interface DataTableProps { columns: Column[]; data: T[]; onRowClick?: (item: T) => void; emptyMessage?: string; isLoading?: boolean; keyField?: string; selectable?: boolean; selectedKeys?: Set; onSelectionChange?: (keys: Set) => void; } export default function DataTable({ columns, data, onRowClick, emptyMessage, isLoading, keyField = 'id', selectable, selectedKeys, onSelectionChange }: DataTableProps) { if (isLoading) { return (
Loading...
); } if (!data.length) { return (
{emptyMessage || 'No data found'}
); } const allKeys = data.map((item) => (item as Record)[keyField] as string); const allSelected = selectable && selectedKeys && allKeys.length > 0 && allKeys.every(k => selectedKeys.has(k)); const toggleAll = () => { if (!onSelectionChange) return; if (allSelected) { onSelectionChange(new Set()); } else { onSelectionChange(new Set(allKeys)); } }; const toggleOne = (key: string) => { if (!onSelectionChange || !selectedKeys) return; const next = new Set(selectedKeys); if (next.has(key)) next.delete(key); else next.add(key); onSelectionChange(next); }; return (
{selectable && ( )} {columns.map(col => ( ))} {data.map((item, i) => { const rowKey = (item as Record)[keyField] as string ?? `row-${i}`; const isSelected = selectable && selectedKeys?.has(rowKey); return ( onRowClick?.(item)} className={`border-b border-slate-700/50 transition-colors hover:bg-blue-500/5 ${onRowClick ? 'cursor-pointer' : ''} ${isSelected ? 'bg-blue-500/10' : ''}`} > {selectable && ( )} {columns.map(col => ( ))} ); })}
{col.label}
{ e.stopPropagation(); toggleOne(rowKey); }} onClick={(e) => e.stopPropagation()} className="rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500 focus:ring-offset-0 cursor-pointer" /> {col.render(item)}
); } export type { Column };