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.6 KB · 83 lines tsx
Raw Blame History
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