TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { requireAdmin } from '@/lib/admin/auth';2import { taxonomyQueue } from '@/lib/admin/queries';3import { proposeTaxonomyNode, taxonomyDecision } from '@/lib/admin/actions';4import { AdminShell, ActionButton, StatusPill, fmtTs, n } from '@/components/admin/shell';56export default async function TaxonomyAdminPage() {7 await requireAdmin();8 const { pending, decided, families } = await taxonomyQueue();9 return (10 <AdminShell current="/admin/taxonomy" title="Taxonomy proposals" subtitle="Category discovery queue (§101): approve to insert a node under the chosen parent; the category becomes available to connectors and pages immediately">11 <div className="grid gap-6 lg:grid-cols-[1fr_320px]">12 <section className="card">13 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Pending ({pending.length})</h2></header>14 <ul className="divide-y divide-border">15 {pending.length === 0 ? <li className="px-4 py-6 text-center text-xs text-subtle">Queue empty. The discovery worker files proposals here when it finds recurring product clusters outside the taxonomy.</li> : null}16 {pending.map((p) => (17 <li key={String(p.id)} className="px-4 py-3 text-sm">18 <div className="flex flex-wrap items-start justify-between gap-3">19 <div>20 <p className="font-medium">{String(p.name)} <span className="font-mono text-xs text-muted">{String(p.proposed_slug)}</span></p>21 <p className="text-xs text-muted">parent {String(p.parent_slug ?? '—')} · est. volume {n(p.volume_estimate)} · {fmtTs(p.created_at)}</p>22 <pre className="mt-1 max-h-32 overflow-auto rounded-md bg-sunken p-2 text-[10px] text-muted">{JSON.stringify(p.evidence ?? {}, null, 1)}</pre>23 </div>24 <div className="flex items-center gap-2">25 <form action={taxonomyDecision} className="flex items-center gap-1">26 <input type="hidden" name="id" value={String(p.id)} />27 <input type="hidden" name="decision" value="approved" />28 <select name="parent" defaultValue={String(p.parent_slug ?? '')} className="rounded-md border border-border bg-sunken px-1.5 py-0.5 text-xs">29 <option value="">(new family)</option>30 {families.map((f) => <option key={String(f.slug)} value={String(f.slug)}>{String(f.name)}</option>)}31 </select>32 <ActionButton label="approve" tone="primary" />33 </form>34 <form action={taxonomyDecision}>35 <input type="hidden" name="id" value={String(p.id)} />36 <input type="hidden" name="decision" value="rejected" />37 <ActionButton label="reject" tone="danger" />38 </form>39 </div>40 </div>41 </li>42 ))}43 </ul>44 <header className="border-y border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Decided</h2></header>45 <ul className="divide-y divide-border text-xs">46 {decided.map((p) => <li key={String(p.id)} className="flex items-center justify-between px-4 py-2"><span>{String(p.name)} <span className="font-mono text-muted">{String(p.proposed_slug)}</span></span><span className="flex items-center gap-2 text-subtle"><StatusPill status={String(p.status)} />{fmtTs(p.decided_at)}</span></li>)}47 </ul>48 </section>49 <section className="card self-start">50 <header className="border-b border-border px-4 py-2.5"><h2 className="text-sm font-semibold">Propose manually</h2></header>51 <form action={proposeTaxonomyNode} className="space-y-2 p-4 text-xs">52 <label className="block text-muted">Slug<input name="slug" required placeholder="riftbound" className="mt-1 w-full rounded-md border border-border bg-sunken px-2 py-1.5 text-sm" /></label>53 <label className="block text-muted">Name<input name="name" required placeholder="Riftbound (League of Legends TCG)" className="mt-1 w-full rounded-md border border-border bg-sunken px-2 py-1.5 text-sm" /></label>54 <label className="block text-muted">Parent<select name="parent" className="mt-1 w-full rounded-md border border-border bg-sunken px-2 py-1.5 text-sm"><option value="">(new family)</option>{families.map((f) => <option key={String(f.slug)} value={String(f.slug)}>{String(f.name)}</option>)}</select></label>55 <ActionButton label="Add to queue" small={false} tone="primary" />56 </form>57 </section>58 </div>59 </AdminShell>60 );61}62