1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader } from '@/components/ui/page-header';4import { Badge } from '@/components/ui/primitives';5import { getCategoryCounts } from '@/lib/queries/markets';6import { FAMILIES, getCategory } from '@rareindex/taxonomy';7import { fmtNum } from '@/lib/format';8import { INDICES } from '@rareindex/taxonomy';910export const metadata: Metadata = { title: 'Categories', description: 'The RareIndex collectible taxonomy: every family and subcategory tracked, with launch phase and live coverage.' };11export const revalidate = 300;1213export default async function CategoriesPage() {14 const counts = await getCategoryCounts();15 const total = (slugs: string[]) => slugs.reduce((a, s) => a + (counts.get(s)?.assets ?? 0), 0);16 const groups = [1, 2, 3].map((phase) => ({ phase, families: FAMILIES.filter((f) => f.phase === phase) }));17 return (18 <div>19 <PageHeader title="Categories" description={`An extensible taxonomy of ${FAMILIES.length} collectible families. Phase 1 is the launch wedge; every family is architected for data from day one. New categories are proposed automatically from market data and approved by an editor.`} />20 {groups.map((g) => (21 <section key={g.phase} className="mb-8">22 <h2 className="mb-3 flex items-center gap-2 text-sm font-semibold">23 Phase {g.phase}24 <span className="text-xs font-normal text-muted">{g.phase === 1 ? 'Launch wedge' : g.phase === 2 ? 'Next expansion' : 'Full universe'}</span>25 <span className="num text-xs font-normal text-subtle">{g.families.length} families</span>26 </h2>27 <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">28 {g.families.map((f) => {29 const assets = total([f.slug, ...f.children.flatMap((c) => [c, ...(getCategory(c)?.children ?? [])])]);30 const idx = INDICES.find((i) => i.ticker === f.index);31 return (32 <div key={f.slug} className="card flex flex-col p-3">33 <div className="flex items-start justify-between gap-2">34 <Link href={`/markets/${f.slug}`} className="text-[13px] font-semibold text-fg hover:underline">35 {f.name}36 </Link>37 {idx ? (38 <Link href={`/rareindex/${idx.ticker}`} className="shrink-0">39 <Badge tone="index">{idx.ticker}</Badge>40 </Link>41 ) : null}42 </div>43 {f.description ? <p className="mt-1 line-clamp-2 text-[11px] text-muted">{f.description}</p> : null}44 {f.children.length ? (45 <ul className="mt-2 flex flex-wrap gap-1">46 {f.children.map((c) => {47 const n = getCategory(c)!;48 const cnt = counts.get(c)?.assets ?? 0;49 return (50 <li key={c}>51 <Link href={`/markets/${c}`} className="rounded-sm bg-inset px-1.5 py-0.5 text-[11px] text-muted hover:text-fg">52 {n.name}53 {cnt ? <span className="num ml-1 text-subtle">{fmtNum(cnt, { compact: true })}</span> : null}54 </Link>55 </li>56 );57 })}58 </ul>59 ) : null}60 <div className="mt-auto flex items-center justify-between pt-2 text-[11px] text-subtle">61 <span className="num">{assets ? `${fmtNum(assets)} assets` : 'no data yet'}</span>62 <span>{f.conditionScale ? `scale: ${f.conditionScale}` : ''}</span>63 </div>64 </div>65 );66 })}67 </div>68 </section>69 ))}70 </div>71 );72}73