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:
1 parent
be1e61d94b
commit
e44174517f
1 file changed
+148
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user