- Created centralized DataTable component with sorting and mobile-responsive columns - Refactored BatchesPage to use DataTable instead of card grid - Improved information density and scannability for operational data - Updated StageBadge styling to match new system
127 lines
5.8 KiB
TypeScript
127 lines
5.8 KiB
TypeScript
import { ReactNode } from 'react';
|
|
import { ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
|
import { cn } from '../../lib/utils';
|
|
import { EmptyState } from './LinearPrimitives'; // Re-use the AuraUI empty state
|
|
|
|
// --- Types ---
|
|
|
|
export interface Column<T> {
|
|
key: keyof T | string; // 'tag', 'strain.name', etc.
|
|
header: string;
|
|
cell: (row: T) => ReactNode; // Custom renderer
|
|
sortable?: boolean;
|
|
className?: string; // e.g. 'text-right', 'w-24'
|
|
hideOnMobile?: boolean;
|
|
}
|
|
|
|
interface DataTableProps<T> {
|
|
data: T[];
|
|
columns: Column<T>[];
|
|
onRowClick?: (row: T) => void;
|
|
isLoading?: boolean;
|
|
emptyState?: ReactNode; // Custom empty state or default
|
|
}
|
|
|
|
// --- Component ---
|
|
|
|
export function DataTable<T extends { id: string | number }>({
|
|
data,
|
|
columns,
|
|
onRowClick,
|
|
isLoading,
|
|
emptyState,
|
|
}: DataTableProps<T>) {
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="w-full border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden bg-white dark:bg-slate-950">
|
|
<div className="border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 px-4 py-3 h-10 flex items-center gap-4">
|
|
{/* Skeleton Header */}
|
|
<div className="h-4 w-24 bg-slate-200 dark:bg-slate-800 rounded animate-pulse" />
|
|
<div className="h-4 w-24 bg-slate-200 dark:bg-slate-800 rounded animate-pulse" />
|
|
<div className="h-4 w-24 bg-slate-200 dark:bg-slate-800 rounded animate-pulse" />
|
|
</div>
|
|
<div>
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="px-4 py-4 border-b border-slate-100 dark:border-slate-800/50 flex items-center gap-4">
|
|
<div className="h-4 w-1/4 bg-slate-100 dark:bg-slate-800 rounded animate-pulse" />
|
|
<div className="h-4 w-1/4 bg-slate-100 dark:bg-slate-800 rounded animate-pulse" />
|
|
<div className="h-4 w-1/4 bg-slate-100 dark:bg-slate-800 rounded animate-pulse" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
if (emptyState) return <>{emptyState}</>;
|
|
return (
|
|
<div className="border border-slate-200 dark:border-slate-800 rounded-lg bg-slate-50/50 dark:bg-slate-950/50 p-12 flex flex-col items-center justify-center text-center">
|
|
<p className="text-slate-500 dark:text-slate-400">No records found</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden bg-white dark:bg-slate-950 shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
{/* Header */}
|
|
<thead className="bg-slate-50 dark:bg-slate-900/50 text-slate-500 dark:text-slate-400 font-medium border-b border-slate-200 dark:border-slate-800">
|
|
<tr>
|
|
{columns.map((col, idx) => (
|
|
<th
|
|
key={String(col.key) + idx}
|
|
className={cn(
|
|
"px-4 py-3 whitespace-nowrap transition-colors",
|
|
col.sortable && "cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-700 dark:hover:text-slate-300",
|
|
col.hideOnMobile && "hidden md:table-cell",
|
|
col.className
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-1">
|
|
{col.header}
|
|
{col.sortable && <ArrowUpDown className="h-3 w-3 opacity-50" />}
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
|
|
{/* Body */}
|
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
|
|
{data.map((row) => (
|
|
<tr
|
|
key={row.id}
|
|
onClick={() => onRowClick && onRowClick(row)}
|
|
className={cn(
|
|
"group transition-colors",
|
|
onRowClick
|
|
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-900/40"
|
|
: "",
|
|
"bg-white dark:bg-slate-950"
|
|
)}
|
|
>
|
|
{columns.map((col, idx) => (
|
|
<td
|
|
key={String(col.key) + idx}
|
|
className={cn(
|
|
"px-4 py-3 align-middle text-slate-700 dark:text-slate-300",
|
|
col.hideOnMobile && "hidden md:table-cell",
|
|
col.className
|
|
)}
|
|
>
|
|
{col.cell(row)}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Optional Footer/Pagination area could go here */}
|
|
</div>
|
|
);
|
|
}
|