feat(mobile): add DataView (TanStack table on lg+, card list below) with cardRender callback
This commit is contained in:
99
src/components/shared/data-view.tsx
Normal file
99
src/components/shared/data-view.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user