"use client"; import { flexRender, getCoreRowModel, getSortedRowModel, getPaginationRowModel, useReactTable, type ColumnDef, type SortingState, type PaginationState, type OnChangeFn, } from "@tanstack/react-table"; import { useState } from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; /** * One table component the whole admin reuses. Headless TanStack Table rendered * as a semantic styled with the app's tokens. Two pagination modes: * - client (default): pass all rows, the table paginates/sorts in memory. * - server (manualPagination + pageCount + pagination/onPaginationChange): * the caller fetches each page; the table just renders + emits page changes. */ export function DataTable({ columns, data, // server pagination manualPagination, pageCount, pagination, onPaginationChange, // ui loading, emptyMessage = "No rows.", onRowClick, pageSizeOptions = [25, 50, 100], }: { columns: ColumnDef[]; data: T[]; manualPagination?: boolean; pageCount?: number; pagination?: PaginationState; onPaginationChange?: OnChangeFn; loading?: boolean; emptyMessage?: string; onRowClick?: (row: T) => void; pageSizeOptions?: number[]; }) { const [sorting, setSorting] = useState([]); const [clientPagination, setClientPagination] = useState({ pageIndex: 0, pageSize: pageSizeOptions[0] ?? 25, }); const table = useReactTable({ data, columns, state: { sorting, pagination: manualPagination ? pagination : clientPagination, }, onSortingChange: setSorting, onPaginationChange: manualPagination ? onPaginationChange : setClientPagination, manualPagination: manualPagination ?? false, pageCount: manualPagination ? pageCount : undefined, getCoreRowModel: getCoreRowModel(), getSortedRowModel: manualPagination ? undefined : getSortedRowModel(), getPaginationRowModel: manualPagination ? undefined : getPaginationRowModel(), }); const state = table.getState().pagination; const totalPages = manualPagination ? (pageCount ?? 1) : table.getPageCount(); return (
{table.getHeaderGroups().map((hg) => ( {hg.headers.map((header) => { const sortable = header.column.getCanSort(); const dir = header.column.getIsSorted(); return ( ); })} ))} {loading ? ( ) : table.getRowModel().rows.length === 0 ? ( ) : ( table.getRowModel().rows.map((row) => ( onRowClick(row.original) : undefined } className={`border-b border-line/60 last:border-0 ${ onRowClick ? "cursor-pointer transition-colors hover:bg-fill/60" : "" }`} > {row.getVisibleCells().map((cell) => ( ))} )) )}
{header.isPlaceholder ? null : ( )}
Loading…
{emptyMessage}
{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
{/* Pagination footer */}
Page {state.pageIndex + 1} of {Math.max(1, totalPages)}
); } /* ── Small formatters reused by columns ──────────────────────────────── */ /** Format a unix-epoch-seconds timestamp as a compact local date-time. */ export function fmtDate(epochSeconds: number): string { if (!epochSeconds) return "—"; const d = new Date(epochSeconds * 1000); return d.toLocaleString(undefined, { year: "2-digit", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } export function truncate(s: string, n = 60): string { return s.length > n ? s.slice(0, n - 1) + "…" : s; }