"use client"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; import { cx } from "@/lib/format"; export interface Column { key: string; header: ReactNode; cell: (row: T) => ReactNode; /** Sort accessor; omit for unsortable columns. */ sort?: (row: T) => number | string | null | undefined; num?: boolean; className?: string; hideBelow?: "sm" | "md" | "lg"; } const HIDE: Record = { sm: "hidden sm:table-cell", md: "hidden md:table-cell", lg: "hidden lg:table-cell" }; /** Dense, client-sortable table with sticky header. Rows are rendered by the caller's cells (which may be live components). */ export function DataTable({ rows, columns, rowKey, defaultSort, defaultDir = "desc", className, maxHeight, emptyText = "Nothing to show yet.", onRowHref }: { rows: T[]; columns: Column[]; rowKey: (row: T) => string; defaultSort?: string; defaultDir?: "asc" | "desc"; className?: string; maxHeight?: string; emptyText?: string; onRowHref?: (row: T) => string }) { const [sortKey, setSortKey] = useState(defaultSort); const [dir, setDir] = useState<"asc" | "desc">(defaultDir); const sorted = useMemo(() => { const col = columns.find((c) => c.key === sortKey); if (!col?.sort) return rows; const acc = col.sort; return [...rows].sort((a, b) => { const va = acc(a); const vb = acc(b); if (va == null && vb == null) return 0; if (va == null) return 1; if (vb == null) return -1; const c = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb)); return dir === "asc" ? c : -c; }); }, [rows, columns, sortKey, dir]); const toggle = (key: string) => { if (sortKey === key) setDir((d) => (d === "asc" ? "desc" : "asc")); else { setSortKey(key); setDir("desc"); } }; return (
{columns.map((c) => ( ))} {sorted.length === 0 && ( )} {sorted.map((r) => ( (window.location.href = onRowHref(r)) : undefined}> {columns.map((c) => ( ))} ))}
{c.sort ? ( ) : ( c.header )}
{emptyText}
{c.cell(r)}
); }