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 { PageHeader } from '@/components/ui/page-header';3import { Segmented } from '@/components/ui/tabs';4import { Card, EmptyState } from '@/components/ui/primitives';5import { Treemap } from '@/components/charts/treemap';6import { getMarketRows } from '@/lib/queries/markets';7import { spEnum, sp1, type SP } from '@/lib/search-params';8import { getCategory } from '@rareindex/taxonomy';910export const metadata: Metadata = { title: 'Market map', description: 'Heatmap of collectible category performance: area by 30-day volume or tracked assets, colour by period change.' };11export const revalidate = 300;1213const PERIODS = ['1d', '7d', '30d', '1y'] as const;14const SIZES = ['volume', 'assets', 'sales'] as const;1516export default async function MarketMapPage({ searchParams }: { searchParams: Promise<SP> }) {17  const sp = await searchParams;18  const period = spEnum(sp, 'p', PERIODS, '30d');19  const size = spEnum(sp, 'size', SIZES, 'volume');20  const parent = sp1(sp, 'family') ?? null;21  const rows = await getMarketRows(parent);22  const key = period === '1d' ? 'change1d' : period === '7d' ? 'change7d' : period === '1y' ? 'change1y' : 'change30d';23  const nodes = rows24    .filter((r) => r.counts.assets > 0)25    .map((r) => {26      const vol = r.counts.volume30dUsd ?? 0;27      const s = size === 'volume' ? vol : size === 'sales' ? r.counts.sales : r.counts.assets;28      return { id: r.node.slug, label: r.node.name, size: s, change: r.snapshot?.[key] ?? null, href: r.node.children.length && !parent ? `/market-map?p=${period}&size=${size}&family=${r.node.slug}` : `/markets/${r.node.slug}`, sublabel: `${r.counts.assets.toLocaleString('en-US')} assets · ${r.counts.sales.toLocaleString('en-US')} sales`, sizeLabel: size === 'volume' ? '30D volume ($)' : size === 'sales' ? 'sales' : 'tracked assets' };29    })30    .filter((n) => n.size > 0);31  const withChange = nodes.filter((n) => n.change !== null).length;32  const q = (p: string, s: string) => `/market-map?p=${p}&size=${s}${parent ? `&family=${parent}` : ''}`;33  return (34    <div>35      <PageHeader kicker="Heatmap"36        crumbs={parent ? [{ label: 'Market map', href: '/market-map' }, { label: getCategory(parent)?.name ?? parent }] : undefined}37        title={parent ? `${getCategory(parent)?.name ?? parent} map` : 'Market map'}38        description="Area is a real activity measure (volume, sales or tracked assets); colour is the period change of the category index — grey means no change is computable yet. Click a family to drill into its subcategories."39        compact40        actions={41          <>42            <Segmented active={period} options={PERIODS.map((p) => ({ id: p, label: p.toUpperCase(), href: q(p, size) }))} />43            <Segmented active={size} options={SIZES.map((s) => ({ id: s, label: s === 'volume' ? 'Volume' : s === 'sales' ? 'Sales' : 'Assets', href: q(period, s) }))} />44          </>45        }46      />47      {nodes.length ? (48        <>49          <Treemap ariaLabel="Market heatmap" nodes={nodes} height={520} limit={period === '1d' ? 0.05 : period === '7d' ? 0.1 : period === '30d' ? 0.15 : 0.4} />50          <p className="mt-2 text-[11px] text-subtle">51            {nodes.length} categories with data · {withChange} with a computed {period.toUpperCase()} change{size === 'volume' && nodes.every((n) => n.size === 0) ? ' · no volume yet, showing assets' : ''}.52          </p>53        </>54      ) : (55        <Card>56          <EmptyState title="No market data yet" description={size === 'volume' ? 'No 30-day volume recorded yet — try sizing by tracked assets.' : 'The map fills as assets, sales and category snapshots are ingested.'} />57        </Card>58      )}59    </div>60  );61}62