← 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>
  );
}