Spaces:
Sleeping
Sleeping
File size: 2,020 Bytes
8f1c2fc 20abf65 8f1c2fc 20abf65 8f1c2fc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import { useMemo, useState } from 'preact/hooks';
/** Generic searchable table. `columns`: [{key, label, render?}]. `rows`: array
* of plain objects. Search matches any column's stringified value. Renders
* markdown-lite backtick/glyph cells as-is (the API sends raw source cells). */
export default function SearchableTable({ columns, rows, placeholder, caption }) {
const [q, setQ] = useState('');
const filtered = useMemo(() => {
const needle = q.trim().toLowerCase();
if (!needle) return rows;
return rows.filter((r) =>
columns.some((c) => String(r[c.key] ?? '').toLowerCase().includes(needle)),
);
}, [q, rows, columns]);
return (
<div class="searchable-table">
<div class="table-toolbar">
<input
type="search"
class="table-search"
placeholder={placeholder || 'Search…'}
value={q}
onInput={(e) => setQ(e.currentTarget.value)}
/>
<span class="table-count">
{filtered.length} / {rows.length} rows
</span>
</div>
{caption && <p class="panel-sub">{caption}</p>}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
{columns.map((c) => (
<th class={c.align === 'right' ? 'num' : ''} key={c.key}>{c.label}</th>
))}
</tr>
</thead>
<tbody>
{filtered.length === 0 && (
<tr>
<td class="empty-note" colSpan={columns.length}>
No rows match “{q}”.
</td>
</tr>
)}
{filtered.map((r, i) => (
<tr key={i}>
{columns.map((c) => (
<td class={c.align === 'right' ? 'num' : ''} key={c.key}>
{c.render ? c.render(r) : r[c.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
|