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%
3.6 KB · 62 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader } from '@/components/ui/page-header';4import { MarketTable } from '@/components/market/market-table';5import { CategoryGrid } from '@/components/market/category-grid';6import { Card, CardHeader } from '@/components/ui/primitives';7import { getCategorySparklines, getMarketRows } from '@/lib/queries/markets';8import { getCategoryThumbnails } from '@/lib/queries/assets';9import { listIndices } from '@/lib/queries/indices';10import { fmtMoney, fmtNum } from '@/lib/format';1112export const metadata: Metadata = { title: 'Markets', description: 'Collectible category markets: index performance, sales, volume, listings, liquidity and market capitalization estimates.' };13export const revalidate = 300;1415export default async function MarketsPage() {16  const [rows, indices] = await Promise.all([getMarketRows(), listIndices()]);17  const active = rows.filter((r) => r.counts.assets > 0);18  const [thumbs, sparklines] = await Promise.all([getCategoryThumbnails(active.map((r) => r.node.slug)), getCategorySparklines(active.map((r) => r.node.slug), 90)]);19  const totals = active.reduce((a, r) => ({ assets: a.assets + r.counts.assets, priced: a.priced + r.counts.priced, sales: a.sales + r.counts.sales, sales30d: a.sales30d + r.counts.sales30d, volume: a.volume + (r.counts.volume30dUsd ?? 0), listings: a.listings + r.counts.listings }), { assets: 0, priced: 0, sales: 0, sales30d: 0, volume: 0, listings: 0 });20  const published = indices.filter((i) => i.latest).length;21  return (22    <div>23      <PageHeader24        kicker="Markets"25        title="Category markets"26        description="One row per collectible family. Index and period changes come from category snapshots; counts come live from the canonical tables. Families without data are listed so coverage is transparent."27        meta={28          <>29            <Link href="/rareindex" className="hover:text-fg">30              {published}/{indices.length} indices published31            </Link>32            <span aria-hidden>·</span>33            <Link href="/market-map" className="hover:text-fg">34              Heatmap view →35            </Link>36            <span aria-hidden>·</span>37            <Link href="/categories" className="hover:text-fg">38              Full taxonomy →39            </Link>40          </>41        }42        stats={[43          { label: 'Families with data', value: fmtNum(active.length), sub: `of ${fmtNum(rows.length)}` },44          { label: 'Tracked assets', value: fmtNum(totals.assets), sub: `${fmtNum(totals.priced)} with valuation` },45          { label: 'Sales (all time)', value: fmtNum(totals.sales) },46          { label: 'Sales · 30D', value: fmtNum(totals.sales30d) },47          { label: 'Volume · 30D', value: totals.volume ? fmtMoney(totals.volume, 'USD', { compact: true }) : '—' },48          { label: 'Active listings', value: fmtNum(totals.listings) },49        ]}50      />51      <section className="mb-6">52        <CategoryGrid rows={active} thumbs={thumbs} sparklines={sparklines} mobileLimit={8} />53      </section>54      <Card className="overflow-hidden">55        <CardHeader title="All families" subtitle="Heat cells are period changes of the category index; grey means not yet computable" />56        <MarketTable rows={rows} />57      </Card>58      <p className="mt-3 text-[11px] leading-relaxed text-subtle">Market capitalization estimates multiply an estimated population by a representative value and are shown only with their confidence label; they are indicative, never precise (§124). Listing prices are not confirmed transactions.</p>59    </div>60  );61}62