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 { 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