tahamajs's picture
download
raw
6.23 kB
// 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.