spb/tools-spboucher-ai
Public
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