SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.7 KB · 62 lines tsx
Raw Blame History
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