← back to Homesonspec
apps/admin/src/app/business/SortableTable.tsx
56 lines
"use client";
import { useState } from "react";
export type Col = { key: string; label: string; num?: boolean; money?: boolean; href?: string };
const fillHref = (tpl: string, row: Row) => tpl.replace(/\{(\w+)\}/g, (_, k) => encodeURIComponent(String(row[k] ?? "")));
export type Row = Record<string, string | number | null>;
const fmt = (v: unknown, money?: boolean) => {
if (v === null || v === undefined || v === "") return "—";
if (typeof v === "number") return (money ? "$" : "") + v.toLocaleString();
return String(v);
};
export default function SortableTable({ cols, rows, initialSort }: { cols: Col[]; rows: Row[]; initialSort?: string }) {
const [sort, setSort] = useState(initialSort ?? cols[0]!.key);
const [dir, setDir] = useState<1 | -1>(-1);
const sorted = [...rows].sort((a, b) => {
const x = a[sort], y = b[sort];
if (x === null || x === undefined) return 1;
if (y === null || y === undefined) return -1;
if (typeof x === "number" && typeof y === "number") return (x - y) * dir;
return String(x).localeCompare(String(y)) * dir;
});
const click = (k: string) => { if (k === sort) setDir((d) => (d === 1 ? -1 : 1)); else { setSort(k); setDir(-1); } };
return (
<div className="overflow-x-auto rounded-xl border border-neutral-200">
<table className="w-full text-sm">
<thead>
<tr className="bg-neutral-50">
{cols.map((c) => (
<th key={c.key} onClick={() => click(c.key)}
className={`cursor-pointer select-none border-b border-neutral-200 px-3 py-2 font-semibold hover:bg-neutral-100 ${c.num ? "text-right" : "text-left"}`}>
{c.label}<span className="ml-1 text-neutral-400">{sort === c.key ? (dir === -1 ? "▼" : "▲") : "↕"}</span>
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((r, i) => (
<tr key={i} className="odd:bg-white even:bg-neutral-50 hover:bg-teal-50/40">
{cols.map((c) => {
const cell = <span className={c.num ? "tabular-nums" : ""}>{fmt(r[c.key], c.money)}</span>;
return (
<td key={c.key} className={`border-b border-neutral-100 px-3 py-1.5 ${c.num ? "text-right" : "text-left"}`}>
{c.href ? <a href={fillHref(c.href, r)} target="_blank" rel="noreferrer" className="text-teal-700 hover:underline">{cell}</a> : cell}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}