| // src/components/genui/DataTableComponent.tsx | |
| import { useState, useMemo } from "react"; | |
| import { Search, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Download } from "lucide-react"; | |
| interface DataTableComponentProps { | |
| title?: string; | |
| columns: { key: string; label: string; sortable?: boolean }[]; | |
| rows?: Record<string, any>[]; | |
| data?: Record<string, any>[]; | |
| pageSize?: number; | |
| searchable?: boolean; | |
| isStreaming?: boolean; | |
| onAction?: (action: string, data: any) => void; | |
| } | |
| export function DataTableComponent({ title = "Data Table", columns, rows, data, pageSize = 10, searchable = true, onAction }: DataTableComponentProps) { | |
| const sourceRows = rows ?? data ?? []; | |
| const [search, setSearch] = useState(""); | |
| const [sortKey, setSortKey] = useState<string | null>(null); | |
| const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); | |
| const [page, setPage] = useState(0); | |
| const filtered = useMemo(() => { | |
| let result = sourceRows; | |
| if (search) { | |
| const q = search.toLowerCase(); | |
| result = result.filter(row => | |
| columns.some(col => String(row[col.key] ?? "").toLowerCase().includes(q)) | |
| ); | |
| } | |
| if (sortKey) { | |
| result = [...result].sort((a, b) => { | |
| const va = a[sortKey] ?? "", vb = b[sortKey] ?? ""; | |
| const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true }); | |
| return sortDir === "asc" ? cmp : -cmp; | |
| }); | |
| } | |
| return result; | |
| }, [sourceRows, search, sortKey, sortDir, columns]); | |
| const totalPages = Math.ceil(filtered.length / pageSize); | |
| const displayed = filtered.slice(page * pageSize, (page + 1) * pageSize); | |
| const toggleSort = (key: string) => { | |
| if (sortKey === key) setSortDir(d => d === "asc" ? "desc" : "asc"); | |
| else { setSortKey(key); setSortDir("asc"); } | |
| }; | |
| const exportCSV = () => { | |
| const header = columns.map(c => c.label).join(","); | |
| const body = filtered.map(r => columns.map(c => `"${String(r[c.key] ?? "").replace(/"/g, "\"\"")}"`).join(",")).join("\n"); | |
| const blob = new Blob([header + "\n" + body], { type: "text/csv" }); | |
| const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `${title.replace(/\s+/g,"_")}.csv`; a.click(); | |
| }; | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="font-semibold text-sm">{title}</h3> | |
| <div className="flex items-center gap-2"> | |
| {searchable && ( | |
| <div className="relative"> | |
| <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-gray-400" /> | |
| <input type="text" placeholder="Search..." value={search} onChange={e => { setSearch(e.target.value); setPage(0); }} className="pl-6 pr-2 py-1 text-xs border rounded-lg dark:bg-gray-700 w-40" /> | |
| </div> | |
| )} | |
| <button onClick={exportCSV} className="p-1 text-gray-400 hover:text-primary" title="Export CSV"><Download className="w-3.5 h-3.5" /></button> | |
| </div> | |
| </div> | |
| <div className="overflow-x-auto border rounded-lg dark:border-gray-700"> | |
| <table className="w-full text-xs"> | |
| <thead> | |
| <tr className="bg-gray-50 dark:bg-gray-800"> | |
| {columns.map(col => ( | |
| <th key={col.key} className={`px-3 py-2 text-left font-semibold text-gray-600 dark:text-gray-300 whitespace-nowrap ${col.sortable ? "cursor-pointer hover:text-primary select-none" : ""}`} onClick={() => col.sortable && toggleSort(col.key)}> | |
| <span className="flex items-center gap-0.5"> | |
| {col.label} | |
| {col.sortable && sortKey === col.key && (sortDir === "asc" ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} | |
| </span> | |
| </th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {displayed.map((row, ri) => ( | |
| <tr key={ri} className="border-t dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"> | |
| {columns.map(col => ( | |
| <td key={col.key} className="px-3 py-1.5 text-gray-700 dark:text-gray-300 max-w-[200px] truncate">{String(row[col.key] ?? "")}</td> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| {totalPages > 1 && ( | |
| <div className="flex items-center justify-between text-xs text-gray-500"> | |
| <span>{filtered.length} rows</span> | |
| <div className="flex items-center gap-1"> | |
| <button onClick={() => setPage(p => Math.max(0, p - 1))} disabled={page === 0} className="p-0.5 disabled:opacity-30"><ChevronLeft className="w-3.5 h-3.5"/></button> | |
| <span>{page + 1} / {totalPages}</span> | |
| <button onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))} disabled={page >= totalPages - 1} className="p-0.5 disabled:opacity-30"><ChevronRight className="w-3.5 h-3.5"/></button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 5.04 kB
- Xet hash:
- a295fa24259249055ef1d525a049ac221dc8f82fa8634aa607d4c7b17cf126f1
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.