100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
|
|
'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>
|
||
|
|
);
|
||
|
|
}
|