| // src/components/data/DataTable.tsx | |
| import { useMemo, useState } from "react"; | |
| import { Search, Table } from "lucide-react"; | |
| type Row = Record<string, unknown>; | |
| interface Column { | |
| key: string; | |
| label: string; | |
| } | |
| interface DataTableProps { | |
| columns?: Column[]; | |
| rows?: Row[]; | |
| data?: Row[]; | |
| title?: string; | |
| onRowClick?: (row: Row) => void; | |
| } | |
| const DEFAULT_COLUMNS = [ | |
| { key: "timestamp", label: "Timestamp" }, | |
| { key: "sampleId", label: "Sample ID" }, | |
| { key: "absorbance", label: "Absorbance (540nm)" }, | |
| { key: "concentration", label: "Concentration (mg/ml)" }, | |
| { key: "status", label: "Status" } | |
| ]; | |
| const DEFAULT_ROWS = [ | |
| { timestamp: "2026-07-05 10:20:11", sampleId: "SMP-001", absorbance: 0.421, concentration: 1.25, status: "Normal" }, | |
| { timestamp: "2026-07-05 10:21:45", sampleId: "SMP-002", absorbance: 0.892, concentration: 2.67, status: "High" }, | |
| { timestamp: "2026-07-05 10:23:02", sampleId: "SMP-003", absorbance: 0.115, concentration: 0.34, status: "Low" }, | |
| { timestamp: "2026-07-05 10:25:19", sampleId: "SMP-004", absorbance: 0.450, concentration: 1.34, status: "Normal" }, | |
| { timestamp: "2026-07-05 10:28:44", sampleId: "SMP-005", absorbance: 1.205, concentration: 3.61, status: "Critical" } | |
| ]; | |
| export function DataTable({ columns, rows, data, title = "Data table", onRowClick }: DataTableProps) { | |
| const [query, setQuery] = useState(""); | |
| const tableRows = rows || data || (columns ? [] : DEFAULT_ROWS); | |
| const tableColumns = useMemo(() => { | |
| if (columns?.length) return columns; | |
| if (!rows && !data) return DEFAULT_COLUMNS; | |
| const keys = Array.from(new Set(tableRows.flatMap((row) => Object.keys(row)))); | |
| return keys.map((key) => ({ key, label: key.replace(/[_-]/g, " ") })); | |
| }, [columns, tableRows, rows, data]); | |
| const visibleRows = tableRows.filter((row) => JSON.stringify(row).toLowerCase().includes(query.toLowerCase())); | |
| if (tableRows.length === 0) { | |
| return ( | |
| <div className="flex h-full min-h-[320px] flex-col items-center justify-center bg-white/30 dark:bg-slate-950/20 backdrop-blur-md p-8 text-center text-slate-500 font-sans border border-white/5 rounded-b-2xl"> | |
| <Table className="mb-3 h-10 w-10 opacity-30 text-teal-500 animate-pulse" /> | |
| <p className="font-extrabold text-slate-800 dark:text-white text-sm">No rows to show</p> | |
| <p className="mt-1 text-xs text-slate-500">Provide rows or data to populate this table.</p> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <section className="flex h-full min-h-[320px] flex-col bg-white/30 dark:bg-slate-950/20 backdrop-blur-md rounded-b-2xl border border-white/5 overflow-hidden shadow-xl select-text font-sans"> | |
| <header className="flex flex-wrap items-center justify-between gap-3 px-6 py-4 border-b border-white/10 dark:border-white/5 bg-white/30 dark:bg-slate-900/30 backdrop-blur-lg select-none"> | |
| <div className="flex items-center gap-2.5 font-extrabold text-sm text-slate-909 dark:text-white"> | |
| <Table className="h-4.5 w-4.5 text-teal-500" /> | |
| <span>{title}</span> | |
| </div> | |
| <label className="flex min-w-[200px] items-center gap-2 rounded-xl border border-white/10 bg-slate-100/50 dark:bg-slate-900/50 px-3 py-1.5 text-xs font-semibold focus-within:border-teal-500/50"> | |
| <Search className="h-4 w-4 text-slate-400" /> | |
| <input | |
| value={query} | |
| onChange={(event) => setQuery(event.target.value)} | |
| placeholder="Search rows..." | |
| className="min-w-0 flex-1 bg-transparent outline-none text-slate-805 dark:text-slate-205 placeholder-slate-550" | |
| /> | |
| </label> | |
| </header> | |
| <div className="min-h-0 flex-1 overflow-auto custom-scrollbar"> | |
| <table className="w-full text-left text-xs"> | |
| <thead className="sticky top-0 bg-slate-100/90 dark:bg-slate-950/90 backdrop-blur-sm text-[10px] font-black uppercase tracking-wider text-slate-500 border-b border-white/10 dark:border-white/5"> | |
| <tr> | |
| {tableColumns.map((column) => ( | |
| <th key={column.key} className="px-4 py-3.5 font-bold border-b border-white/10 dark:border-white/5 select-none">{column.label}</th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-white/5"> | |
| {visibleRows.map((row, index) => ( | |
| <tr | |
| key={index} | |
| onClick={() => onRowClick?.(row as Row)} | |
| className={`transition-colors duration-200 border-b border-white/5 ${onRowClick ? "cursor-pointer hover:bg-teal-500/10" : "hover:bg-white/45 dark:hover:bg-slate-800/40"}`} | |
| > | |
| {tableColumns.map((column) => ( | |
| <td key={column.key} className="max-w-xs truncate px-4 py-3 font-semibold text-slate-700 dark:text-slate-250"> | |
| {formatCell((row as any)[column.key])} | |
| </td> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| ); | |
| } | |
| function formatCell(value: unknown) { | |
| if (value == null) return "-"; | |
| if (typeof value === "boolean") return value ? "Yes" : "No"; | |
| if (typeof value === "object") return JSON.stringify(value); | |
| const strVal = String(value); | |
| if (strVal === "Normal") { | |
| return <span className="px-2 py-0.5 rounded-lg text-[9px] font-black uppercase tracking-wider bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20">Normal</span>; | |
| } | |
| if (strVal === "High") { | |
| return <span className="px-2 py-0.5 rounded-lg text-[9px] font-black uppercase tracking-wider bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20">High</span>; | |
| } | |
| if (strVal === "Low") { | |
| return <span className="px-2 py-0.5 rounded-lg text-[9px] font-black uppercase tracking-wider bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20">Low</span>; | |
| } | |
| if (strVal === "Critical") { | |
| return <span className="px-2 py-0.5 rounded-lg text-[9px] font-black uppercase tracking-wider bg-rose-500/10 text-rose-600 dark:text-rose-400 border border-rose-500/20">Critical</span>; | |
| } | |
| return strVal; | |
| } | |
| export default DataTable; | |
Xet Storage Details
- Size:
- 6.23 kB
- Xet hash:
- cb85ffd47bf42056978cf3d8ed6905ca4916fbc0e537339096896a519ed9e6ec
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.