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