TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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 { getCategoryThumbnails } from '@/lib/queries/assets';7import { Thumb } from '@/components/market/bits';8import { FAMILIES, getCategory } from '@rareindex/taxonomy';9import { fmtNum } from '@/lib/format';10import { INDICES } from '@rareindex/taxonomy';1112export const metadata: Metadata = { title: 'Categories', description: 'The RareIndex collectible taxonomy: every family and subcategory tracked, with launch phase and live coverage.' };13export const revalidate = 300;1415export default async function CategoriesPage() {16 const counts = await getCategoryCounts();17 const thumbs = await getCategoryThumbnails(FAMILIES.flatMap((f) => [f.slug, ...f.children]));18 const total = (slugs: string[]) => slugs.reduce((a, s) => a + (counts.get(s)?.assets ?? 0), 0);19 const groups = [1, 2, 3].map((phase) => ({ phase, families: FAMILIES.filter((f) => f.phase === phase) }));20 return (21 <div>22 <PageHeader kicker="Taxonomy" 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.`} />23 {groups.map((g) => (24 <section key={g.phase} className="mb-8">25 <h2 className="mb-3 flex items-center gap-2 text-sm font-semibold">26 Phase {g.phase}27 <span className="text-xs font-normal text-muted">{g.phase === 1 ? 'Launch wedge' : g.phase === 2 ? 'Next expansion' : 'Full universe'}</span>28 <span className="num text-xs font-normal text-subtle">{g.families.length} families</span>29 </h2>30 <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">31 {g.families.map((f) => {32 const assets = total([f.slug, ...f.children.flatMap((c) => [c, ...(getCategory(c)?.children ?? [])])]);33 const idx = INDICES.find((i) => i.ticker === f.index);34 return (35 <div key={f.slug} className="card flex flex-col p-3">36 <div className="flex items-start justify-between gap-2">37 <Link href={`/markets/${f.slug}`} className="flex min-w-0 items-center gap-2 text-[13px] font-semibold text-fg hover:underline">38 <Thumb src={thumbs.get(f.slug) ?? f.children.map((c) => thumbs.get(c)).find(Boolean)} alt="" size={28} rounded="rounded-md" />39 <span className="truncate">{f.name}</span>40 </Link>41 {idx ? (42 <Link href={`/rareindex/${idx.ticker}`} className="shrink-0">43 <Badge tone="index">{idx.ticker}</Badge>44 </Link>45 ) : null}46 </div>47 {f.description ? <p className="mt-1 line-clamp-2 text-[11px] text-muted">{f.description}</p> : null}48 {f.children.length ? (49 <ul className="mt-2 flex flex-wrap gap-1">50 {f.children.map((c) => {51 const n = getCategory(c)!;52 const cnt = counts.get(c)?.assets ?? 0;53 return (54 <li key={c}>55 <Link href={`/markets/${c}`} className="rounded-sm bg-inset px-1.5 py-0.5 text-[11px] text-muted hover:text-fg">56 {n.name}57 {cnt ? <span className="num ml-1 text-subtle">{fmtNum(cnt, { compact: true })}</span> : null}58 </Link>59 </li>60 );61 })}62 </ul>63 ) : null}64 <div className="mt-auto flex items-center justify-between pt-2 text-[11px] text-subtle">65 <span className="num">{assets ? `${fmtNum(assets)} assets` : 'no data yet'}</span>66 {assets ? (67 <Link href={`/markets/${f.slug}#assets`} className="text-muted hover:text-fg">68 Browse →69 </Link>70 ) : (71 <span>{f.conditionScale ? `scale: ${f.conditionScale}` : ''}</span>72 )}73 </div>74 </div>75 );76 })}77 </div>78 </section>79 ))}80 </div>81 );82}83