SPB Git
3.5 KB · 30 lines tsx
1import Link from "next/link";2import { listTools } from "@/lib/admin/tools";3import { fmtDate, label } from "@/lib/utils";4import { Badge } from "@/components/ui/badge";5export default async function AdminTools({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {6  const sp = await searchParams; const res = await listTools({ q: sp.q, status: sp.status, visibility: sp.visibility, linkState: sp.linkState, depth: sp.depth, category: sp.category, page: Number(sp.page ?? 1), sort: sp.sort as any });7  const qs = (patch: Record<string, string | undefined>) => { const p = new URLSearchParams(); for (const [k, v] of Object.entries({ ...sp, ...patch })) if (v) p.set(k, v); return `/admin/tools?${p}`; };8  return (9    <div>10      <div className="flex flex-wrap items-end justify-between gap-3"><div><h1 className="display h1">Tools</h1><p className="mt-1 text-sm text-fg-2">{res.total} records</p></div><Link href="/admin/tools/new" className="btn-primary">New record</Link></div>11      <form className="mt-4 flex flex-wrap gap-2" method="get">12        <input name="q" defaultValue={sp.q} placeholder="Search name, slug, URL" className="input w-56" aria-label="Search records" />13        {[["status", ["draft", "reviewed", "published", "deprecated"]], ["visibility", ["public", "private"]], ["linkState", ["ok", "redirected", "broken", "unchecked"]], ["depth", ["deep", "standard"]], ["sort", ["updated", "name", "quality", "checked"]]].map(([k, opts]) => (14          <select key={k as string} name={k as string} defaultValue={sp[k as string] ?? ""} className="input w-40" aria-label={k as string}><option value="">{k as string}: any</option>{(opts as string[]).map((o) => <option key={o} value={o}>{label(o)}</option>)}</select>15        ))}16        <button className="btn-ghost">Filter</button>17      </form>18      <div className="mt-4 overflow-x-auto rounded-2xl border border-line bg-bg-2">19        <table className="table min-w-[60rem]"><thead><tr><th>Name</th><th>Category</th><th>Status</th><th>Depth</th><th>Quality</th><th>Links</th><th>Checked</th><th>Owner</th><th>Updated</th></tr></thead>20          <tbody>{res.items.map((t: any) => (21            <tr key={t.id} className="hover:bg-bg-3/50"><td><Link href={`/admin/tools/${t.id}`} className="font-medium hover:text-accent">{t.name}</Link><span className="block text-xs text-fg-3">{t.slug}{t.visibility === "private" ? " · private" : ""}{t.featured ? " · featured" : ""}</span></td><td className="text-fg-2">{t.category ?? "—"}</td>22              <td><Badge tone={t.editorialStatus === "published" ? "ok" : t.editorialStatus === "deprecated" ? "bad" : "warn"}>{t.editorialStatus}</Badge></td><td>{label(t.depth)}</td><td><span className={t.qualityScore < 40 ? "text-bad" : t.qualityScore < 70 ? "text-warn" : "text-ok"}>{t.qualityScore}</span></td>23              <td><Badge tone={t.linkState === "ok" ? "ok" : t.linkState === "broken" ? "bad" : "neutral"}>{label(t.linkState)}</Badge></td><td className="text-fg-3">{fmtDate(t.checkedAt)}</td><td className="text-fg-2">{t.useState ?? "—"}</td><td className="text-fg-3">{fmtDate(t.updatedAt)}</td></tr>24          ))}</tbody></table>25      </div>26      <div className="mt-4 flex items-center gap-2 text-sm">{res.page > 1 && <Link href={qs({ page: String(res.page - 1) })} className="btn-ghost">Previous</Link>}<span className="text-fg-3">Page {res.page} / {res.pages}</span>{res.page < res.pages && <Link href={qs({ page: String(res.page + 1) })} className="btn-ghost">Next</Link>}</div>27    </div>28  );29}30
Files · main
Menu

Theme