feat(mobile): add DataView (TanStack table on lg+, card list below) with cardRender callback

This commit is contained in:
Matt Ciaccio
2026-04-29 14:27:17 +02:00
parent 41ae8a328f
commit 34916d855e

View File

@@ -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 `<Drawer>` opened by
* the caller's headerSlot — this primitive doesn't enforce a sort UI.
*/
export function DataView<TData>({
table,
cardRender,
headerSlot,
emptyState,
className,
}: {
table: TanstackTable<TData>;
cardRender: (row: Row<TData>) => ReactNode;
headerSlot?: ReactNode;
emptyState?: ReactNode;
className?: string;
}) {
const rows = table.getRowModel().rows;
const isEmpty = rows.length === 0;
return (
<div className={cn('flex flex-col gap-3', className)}>
{headerSlot ? <div>{headerSlot}</div> : null}
{/* Desktop: TanStack table */}
<div className="hidden lg:block">
<Table>
<TableHeader>
{table.getHeaderGroups().map((group) => (
<TableRow key={group.id}>
{group.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isEmpty ? (
<TableRow>
<TableCell colSpan={table.getAllColumns().length} className="text-center py-8">
{emptyState ?? 'No results.'}
</TableCell>
</TableRow>
) : (
rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Mobile: card list */}
<ul className="lg:hidden flex flex-col gap-2">
{isEmpty ? (
<li className="rounded-md border border-border bg-card p-4 text-center text-sm text-muted-foreground">
{emptyState ?? 'No results.'}
</li>
) : (
rows.map((row) => (
<li key={row.id} className="rounded-md border border-border bg-card p-3 shadow-xs">
{cardRender(row)}
</li>
))
)}
</ul>
</div>
);
}