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.
This commit is contained in:
openhands committed 2026-08-15 13:58:04 +02:00
1 parent be1e61d94b
commit e44174517f
1 file changed
+148
+148
View File
@@ -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 (
<div className="space-y-4">
{searchableColumns.length > 0 && (
<Input
placeholder={searchPlaceholder}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="max-w-sm"
/>
)}
<Table>
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.key}
className={column.width ? `w-[${column.width}]` : ""}
onClick={column.sortable ? () => handleSort(column.key) : undefined}
style={{ cursor: column.sortable ? "pointer" : "default" }}
>
<div className="flex items-center gap-2">
{column.label}
{column.sortable && (
<span className="text-xs text-gray-400">
{sortConfig?.key === column.key && sortConfig.direction === "asc" ? "↑" : "↓"}
</span>
)}
</div>
</TableHead>
))}
{actions.length > 0 && (
<TableHead className="w-[100px]">Actions</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length + (actions.length > 0 ? 1 : 0)} className="text-center py-8">
Loading...
</TableCell>
</TableRow>
) : sortedData.length === 0 ? (
<TableRow>
<TableCell colSpan={columns.length + (actions.length > 0 ? 1 : 0)} className="text-center py-8 text-gray-500">
No data found
</TableCell>
</TableRow>
) : (
sortedData.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column.key}>
{column.render ? column.render(row[column.key], row) : row[column.key]}
</TableCell>
))}
{actions.length > 0 && (
<TableCell>
<div className="flex gap-2">
{actions.map((action, actionIndex) => (
<Button
key={actionIndex}
size="sm"
variant={action.variant || "outline"}
onClick={() => action.onClick(row)}
>
{action.label}
</Button>
))}
</div>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)
}