DataTable.jsx
1 import { 2 useReactTable, 3 getCoreRowModel, 4 getSortedRowModel, 5 getFilteredRowModel, 6 flexRender, 7 } from '@tanstack/react-table'; 8 import { useState } from 'react'; 9 10 /** 11 * Sortable, filterable data table. 12 * columns: TanStack Table column def array 13 * data: array of row objects 14 * filterPlaceholder: optional search box placeholder 15 * maxRows: optional truncation (shows "and N more" footer) 16 */ 17 export default function DataTable({ columns, data = [], filterPlaceholder, maxRows }) { 18 const [sorting, setSorting] = useState([]); 19 const [globalFilter, setGlobalFilter] = useState(''); 20 21 const displayData = maxRows ? data.slice(0, maxRows) : data; 22 const hidden = maxRows ? Math.max(0, data.length - maxRows) : 0; 23 24 const table = useReactTable({ 25 data: displayData, 26 columns, 27 state: { sorting, globalFilter }, 28 onSortingChange: setSorting, 29 onGlobalFilterChange: setGlobalFilter, 30 getCoreRowModel: getCoreRowModel(), 31 getSortedRowModel: getSortedRowModel(), 32 getFilteredRowModel: getFilteredRowModel(), 33 }); 34 35 return ( 36 <div> 37 {filterPlaceholder && ( 38 <input 39 value={globalFilter} 40 onChange={e => setGlobalFilter(e.target.value)} 41 placeholder={filterPlaceholder} 42 className="mb-3 w-full rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5 43 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:border-sky-500" 44 /> 45 )} 46 <div className="overflow-x-auto rounded-lg border border-slate-700"> 47 <table className="w-full text-sm"> 48 <thead className="bg-slate-800 text-slate-400 uppercase text-xs tracking-wider"> 49 {table.getHeaderGroups().map(hg => ( 50 <tr key={hg.id}> 51 {hg.headers.map(h => ( 52 <th 53 key={h.id} 54 onClick={h.column.getToggleSortingHandler()} 55 className={`px-4 py-2.5 text-left select-none ${ 56 h.column.getCanSort() ? 'cursor-pointer hover:text-slate-200' : '' 57 }`} 58 > 59 {flexRender(h.column.columnDef.header, h.getContext())} 60 {{ asc: ' ↑', desc: ' ↓' }[h.column.getIsSorted()] ?? ''} 61 </th> 62 ))} 63 </tr> 64 ))} 65 </thead> 66 <tbody className="divide-y divide-slate-700/50"> 67 {table.getRowModel().rows.map((row, i) => ( 68 <tr key={row.id} className={i % 2 === 0 ? 'bg-slate-900' : 'bg-slate-800/40'}> 69 {row.getVisibleCells().map(cell => ( 70 <td key={cell.id} className="px-4 py-2.5 text-slate-300"> 71 {flexRender(cell.column.columnDef.cell, cell.getContext())} 72 </td> 73 ))} 74 </tr> 75 ))} 76 {table.getRowModel().rows.length === 0 && ( 77 <tr> 78 <td colSpan={columns.length} className="px-4 py-6 text-center text-slate-500"> 79 No data 80 </td> 81 </tr> 82 )} 83 </tbody> 84 </table> 85 </div> 86 {hidden > 0 && ( 87 <p className="mt-2 text-xs text-slate-500 text-right">… and {hidden} more rows</p> 88 )} 89 </div> 90 ); 91 }