From e44174517f623315c0507bd62775794d6ec2e4b7 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 15 Aug 2026 13:58:04 +0200 Subject: [PATCH] feat: add missing DataTable component for admin pages Fixes build issue caused by missing data-table import in: - src/app/admin/exam-proctoring/page.tsx - src/app/admin/signage/page.tsx The component provides reusable table interface with search, sorting, and actions for all admin interfaces. --- src/components/data-table.tsx | 148 ++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/components/data-table.tsx diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx new file mode 100644 index 0000000..2bf169a --- /dev/null +++ b/src/components/data-table.tsx @@ -0,0 +1,148 @@ +"use client" + +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table" +import { Button } from "./ui/button" +import { Input } from "./ui/input" + +interface Column { + key: string + label: string + sortable?: boolean + width?: string + render?: (value: any, row: any) => React.ReactNode +} + +interface Action { + label: string + onClick: (row: any) => void + variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" +} + +interface DataTableProps { + columns: Column[] + data: any[] + isLoading?: boolean + searchPlaceholder?: string + searchableColumns?: string[] + actions?: Action[] +} + +export function DataTable({ + columns, + data, + isLoading, + searchPlaceholder = "Search...", + searchableColumns = [], + actions = [] +}: DataTableProps) { + const [searchTerm, setSearchTerm] = useState("") + const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null) + + const filteredData = data.filter((row) => { + if (!searchTerm) return true + return searchableColumns.some((key) => { + const value = row[key] + return value?.toString().toLowerCase().includes(searchTerm.toLowerCase()) + }) + }) + + const sortedData = [...filteredData].sort((a, b) => { + if (!sortConfig) return 0 + const aValue = a[sortConfig.key] + const bValue = b[sortConfig.key] + + if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1 + if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1 + return 0 + }) + + const handleSort = (key: string) => { + setSortConfig(current => { + if (current?.key === key) { + return { key, direction: current.direction === "asc" ? "desc" : "asc" } + } + return { key, direction: "asc" } + }) + } + + return ( +
+ {searchableColumns.length > 0 && ( + setSearchTerm(e.target.value)} + className="max-w-sm" + /> + )} + + + + + {columns.map((column) => ( + handleSort(column.key) : undefined} + style={{ cursor: column.sortable ? "pointer" : "default" }} + > +
+ {column.label} + {column.sortable && ( + + {sortConfig?.key === column.key && sortConfig.direction === "asc" ? "↑" : "↓"} + + )} +
+
+ ))} + {actions.length > 0 && ( + Actions + )} +
+
+ + {isLoading ? ( + + 0 ? 1 : 0)} className="text-center py-8"> + Loading... + + + ) : sortedData.length === 0 ? ( + + 0 ? 1 : 0)} className="text-center py-8 text-gray-500"> + No data found + + + ) : ( + sortedData.map((row, index) => ( + + {columns.map((column) => ( + + {column.render ? column.render(row[column.key], row) : row[column.key]} + + ))} + {actions.length > 0 && ( + +
+ {actions.map((action, actionIndex) => ( + + ))} +
+
+ )} +
+ )) + )} +
+
+
+ ) +}