import { flexRender, type Cell, type Row, type Table, } from '@tanstack/react-table' import { Database } from 'lucide-react' import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty' import { Skeleton } from '@/components/ui/skeleton' interface MobileCardListProps { table: Table isLoading?: boolean emptyTitle?: string emptyDescription?: string getRowKey?: (row: Row) => string | number getRowClassName?: (row: Row) => string | undefined } interface MobileColumnMeta { label?: string mobileTitle?: boolean mobileBadge?: boolean mobileHidden?: boolean } function getCellMeta( cell: Cell ): MobileColumnMeta | undefined { return cell.column.columnDef.meta as MobileColumnMeta | undefined } function getCellLabel(cell: Cell): string | null { const meta = getCellMeta(cell) if (meta?.label) return meta.label const header = cell.column.columnDef.header return typeof header === 'string' ? header : null } function renderCellContent(cell: Cell): React.ReactNode { const cellRenderer = cell.column.columnDef.cell if (cellRenderer) { return flexRender(cellRenderer, cell.getContext()) } return cell.getValue() as React.ReactNode } function ListSkeleton() { return (
{[1, 2, 3, 4, 5].map((i) => (
))}
) } function FallbackListSkeleton() { return (
{[1, 2, 3, 4, 5].map((i) => (
{[1, 2, 3].map((j) => (
))}
))}
) } /** * Compact list row — structured layout with title header + side-by-side fields. * Used when columns define mobileTitle or mobileBadge meta. * * Visual structure per row: * [Title content] [Badge] * [Field1 label] [Field2 label] * [Field1 value] [Field2 value] * [Actions ⋯] */ function CompactRow({ row }: { row: Row }) { const allCells = row .getVisibleCells() .filter((cell) => cell.column.id !== 'select') const titleCell = allCells.find((c) => getCellMeta(c)?.mobileTitle) const badgeCell = allCells.find((c) => getCellMeta(c)?.mobileBadge) const actionsCell = allCells.find((c) => c.column.id === 'actions') const fieldCells = allCells.filter( (c) => c !== titleCell && c !== badgeCell && c !== actionsCell && !getCellMeta(c)?.mobileHidden ) return ( <> {/* Row 1: Title + Badge */}
{titleCell && (
{renderCellContent(titleCell)}
)} {badgeCell && (
{renderCellContent(badgeCell)}
)}
{/* Row 2: Key fields wrap into compact columns instead of squeezing */} {fieldCells.length > 0 && (
{fieldCells.map((cell) => { const label = getCellLabel(cell) return (
{label && (
{label}
)}
{renderCellContent(cell) ?? '-'}
) })}
)} {/* Actions */} {actionsCell && (
{renderCellContent(actionsCell)}
)} ) } /** * Fallback list row — condensed label:value pairs for tables without * mobileTitle/mobileBadge. Still respects mobileHidden. */ function FallbackRow({ row }: { row: Row }) { const allCells = row .getVisibleCells() .filter((cell) => cell.column.id !== 'select') const actionsCell = allCells.find((c) => c.column.id === 'actions') const contentCells = allCells.filter( (c) => c.column.id !== 'actions' && !getCellMeta(c)?.mobileHidden ) return ( <> {contentCells.map((cell) => { const label = getCellLabel(cell) const content = renderCellContent(cell) if (!label) { return (
{content}
) } return (
{label}
{content ?? '-'}
) })} {actionsCell && (
{renderCellContent(actionsCell)}
)} ) } /** * Mobile-optimized list view for table data. * * Renders rows inside a single bordered container with dividers — * a Vercel/Stripe-style list rather than individual cards. * * Column meta extensions: * - `mobileTitle` — card header (left, larger text) * - `mobileBadge` — inline with title (right, e.g. status badge) * - `mobileHidden` — hidden on mobile * * When mobileTitle or mobileBadge is set on any column, uses a structured * two-tier layout: title+badge header, then 2 key fields side-by-side. * Otherwise falls back to a condensed single-column label:value list. */ export function MobileCardList(props: MobileCardListProps) { const { table, isLoading = false, emptyTitle, emptyDescription, getRowKey, getRowClassName, } = props const { t } = useTranslation() const resolvedEmptyTitle = emptyTitle ?? t('No Data') const resolvedEmptyDescription = emptyDescription ?? t('No data available') const hasCompactMeta = table.getVisibleLeafColumns().some((col) => { const meta = col.columnDef.meta as MobileColumnMeta | undefined return meta?.mobileTitle || meta?.mobileBadge }) if (isLoading) { return hasCompactMeta ? : } const rows = table.getRowModel().rows if (!rows || rows.length === 0) { return (
{resolvedEmptyTitle} {resolvedEmptyDescription}
) } const RowComponent = hasCompactMeta ? CompactRow : FallbackRow return (
{rows.map((row) => { const key = getRowKey ? getRowKey(row) : row.id return (
) })}
) }