diff --git a/src/components/shared/data-view.tsx b/src/components/shared/data-view.tsx new file mode 100644 index 0000000..b5c9ac1 --- /dev/null +++ b/src/components/shared/data-view.tsx @@ -0,0 +1,99 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { flexRender, type Table as TanstackTable, type Row } from '@tanstack/react-table'; + +import { cn } from '@/lib/utils'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; + +/** + * Renders the same TanStack `Table` instance as a desktop table on `lg+` and + * as a card list on mobile, using `cardRender(row)` for the per-row card body. + * + * Filters and sort live above the rendered rows; callers pass them as + * `headerSlot`. On desktop the rows are sortable via column header clicks + * (TanStack default); on mobile, sort is exposed via a `` opened by + * the caller's headerSlot — this primitive doesn't enforce a sort UI. + */ +export function DataView({ + table, + cardRender, + headerSlot, + emptyState, + className, +}: { + table: TanstackTable; + cardRender: (row: Row) => ReactNode; + headerSlot?: ReactNode; + emptyState?: ReactNode; + className?: string; +}) { + const rows = table.getRowModel().rows; + const isEmpty = rows.length === 0; + + return ( +
+ {headerSlot ?
{headerSlot}
: null} + + {/* Desktop: TanStack table */} +
+ + + {table.getHeaderGroups().map((group) => ( + + {group.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {isEmpty ? ( + + + {emptyState ?? 'No results.'} + + + ) : ( + rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + )} + +
+
+ + {/* Mobile: card list */} +
    + {isEmpty ? ( +
  • + {emptyState ?? 'No results.'} +
  • + ) : ( + rows.map((row) => ( +
  • + {cardRender(row)} +
  • + )) + )} +
+
+ ); +}