import type { Row, Table } from '@tanstack/react-table'
import { Database } from 'lucide-react'
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import { tableHasCompactMeta } from './card-cell-utils'
import { CardRowContent } from './card-row-content'
/** Helpers passed to a custom {@link DataTableCardGridProps.renderCard}. */
export type DataTableCardHelpers = {
/**
* Whether the table declares compact card meta (`mobileTitle`/`mobileBadge`).
* Provided so custom renderers can match the default layout decision.
*/
compact: boolean
/**
* Row selection state captured before entering memoized custom card renderers.
*/
isSelected: boolean
}
export interface DataTableCardGridProps {
table: Table
isLoading?: boolean
emptyTitle?: string
emptyDescription?: string
emptyIcon?: React.ReactNode
getRowKey?: (row: Row) => string | number
getRowClassName?: (row: Row) => string | undefined
/**
* Custom card renderer. When omitted, cards render generically from the
* column definitions via {@link CardRowContent} (driven by column meta).
*/
renderCard?: (
row: Row,
helpers: DataTableCardHelpers
) => React.ReactNode
/**
* Responsive grid className override. Defaults to a 1/2/3-column grid.
*/
gridClassName?: string
/** Stable key prefix for skeleton cards. */
skeletonKeyPrefix?: string
}
const DEFAULT_GRID_CLASSNAME =
'grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3'
function CardGridSkeleton(props: {
gridClassName?: string
keyPrefix?: string
}) {
const prefix = props.keyPrefix ?? 'card-skeleton'
return (
{[1, 2, 3, 4, 5, 6].map((i) => (
{[1, 2, 3, 4].map((j) => (
))}
))}
)
}
/**
* Desktop card view for table data — a responsive grid of bordered cards.
*
* Renders the same per-row content as {@link MobileCardList} (via
* {@link CardRowContent}) unless a custom `renderCard` is supplied. This keeps
* the card view reusable across any table with zero per-feature work while
* still allowing a bespoke card design when desired.
*
* The default generic card omits the `select` column. Custom `renderCard`
* implementations can use `helpers.isSelected` to keep selection UI in sync.
*/
export function DataTableCardGrid(props: DataTableCardGridProps) {
const { t } = useTranslation()
const resolvedEmptyTitle = props.emptyTitle ?? t('No Data')
const resolvedEmptyDescription =
props.emptyDescription ?? t('No data available')
const visibleColumns = props.table.getVisibleLeafColumns()
const compact = React.useMemo(
() => tableHasCompactMeta(props.table),
// eslint-disable-next-line react-hooks/exhaustive-deps
[visibleColumns]
)
if (props.isLoading) {
return (
)
}
const rows = props.table.getRowModel().rows
if (!rows || rows.length === 0) {
return (