Sanitized mirror from private repository - 2026-04-19 15:28:05 UTC
This commit is contained in:
126
dashboard/ui/components/data-table.tsx
Normal file
126
dashboard/ui/components/data-table.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export interface Column<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
render?: (row: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface DataTableProps<T> {
|
||||
data: T[];
|
||||
columns: Column<T>[];
|
||||
searchKey?: string;
|
||||
filterKey?: string;
|
||||
filterOptions?: string[];
|
||||
actions?: (row: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function DataTable<T extends Record<string, any>>({
|
||||
data,
|
||||
columns,
|
||||
searchKey,
|
||||
filterKey,
|
||||
filterOptions,
|
||||
actions,
|
||||
}: DataTableProps<T>) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let rows = data;
|
||||
if (search && searchKey) {
|
||||
const q = search.toLowerCase();
|
||||
rows = rows.filter((r) =>
|
||||
String(r[searchKey] ?? "")
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
);
|
||||
}
|
||||
if (filter !== "all" && filterKey) {
|
||||
rows = rows.filter((r) => String(r[filterKey]) === filter);
|
||||
}
|
||||
return rows;
|
||||
}, [data, search, searchKey, filter, filterKey]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{searchKey && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="h-8 rounded-lg glass-input px-3 text-sm text-foreground placeholder:text-muted-foreground/50 w-64"
|
||||
/>
|
||||
)}
|
||||
{filterKey && filterOptions && (
|
||||
<select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="h-8 rounded-lg glass-input px-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
{filterOptions.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/[0.06] overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="glass-table-header border-b border-white/[0.06]">
|
||||
{columns.map((col) => (
|
||||
<TableHead key={col.key} className="text-sm text-muted-foreground/80">
|
||||
{col.label}
|
||||
</TableHead>
|
||||
))}
|
||||
{actions && <TableHead className="text-sm text-muted-foreground/80 w-24">Actions</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length + (actions ? 1 : 0)}
|
||||
className="text-center text-xs text-muted-foreground py-6"
|
||||
>
|
||||
No results
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filtered.map((row, i) => (
|
||||
<TableRow key={i} className="glass-table-row border-b border-white/[0.04]">
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col.key} className="text-sm">
|
||||
{col.render
|
||||
? col.render(row)
|
||||
: String(row[col.key] ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
{actions && (
|
||||
<TableCell className="text-sm">{actions(row)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user