TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { Suspense } from 'react';2import Link from 'next/link';3import { Hero } from '@/components/home/hero';4import { IndexTape } from '@/components/market/index-strip';5import { AssetRail, LatestTransactions, LotsRail, MarketInsights, RadarRail, SalesRail } from '@/components/home/sections';6import { CategoryGrid } from '@/components/market/category-grid';7import { TileSkeleton } from '@/components/market/asset-tile';8import { attachGuidePrices, getCategoryThumbnails, getLatestObservationsFeed, rankedAssets } from '@/lib/queries/assets';9import { fmtNum } from '@/lib/format';10import { Card, Delta, Skeleton, TableSkeleton } from '@/components/ui/primitives';11import { getLatestSales, getSiteStats } from '@/lib/queries/site';12import { getIndexSeries, listIndices } from '@/lib/queries/indices';13import { getCategorySparklines, getMarketRows } from '@/lib/queries/markets';14import { getTopSales, listLots, listRadar } from '@/lib/queries/market-lists';15import { LineChart } from '@/components/charts/line-chart';1617export const revalidate = 120;1819export default async function HomePage() {20 const [stats, indices] = await Promise.all([getSiteStats(), listIndices()]);21 return (22 <div className="-mx-4 sm:-mx-6">23 <div className="px-4 sm:px-6">24 <Hero stats={stats} />25 </div>26 <div className="mt-8">27 <IndexTape indices={indices} />28 </div>29 {/* §195 hierarchy: hero + search → live stats (in hero) → RARE tape → global index → market snapshot →30 trending → record sales → Rare Radar → value opportunities → ending auctions → latest sales → categories → long tail */}31 <div className="flex flex-col gap-10 px-4 pt-8 sm:px-6">32 <Suspense fallback={<Skeleton className="h-72 w-full" />}>33 <FlagshipPanel />34 </Suspense>35 <Suspense fallback={<TableSkeleton rows={8} />}>36 <Snapshot />37 </Suspense>38 <Suspense fallback={<RailFallback />}>39 <Discovery />40 </Suspense>41 <Suspense fallback={<TableSkeleton rows={10} />}>42 <Latest />43 </Suspense>44 <Suspense fallback={<Skeleton className="h-40" />}>45 <Categories />46 </Suspense>47 <Suspense fallback={<RailFallback />}>48 <LongTail />49 </Suspense>50 </div>51 </div>52 );53}5455function RailFallback() {56 return (57 <div className="flex flex-col gap-10">58 {[0, 1].map((k) => (59 <div key={k}>60 <Skeleton className="mb-3 h-5 w-40" />61 <div className="rail lg:mx-0 lg:grid lg:grid-cols-4 lg:overflow-visible lg:px-0 xl:grid-cols-6 2xl:grid-cols-8 lg:[&>*]:w-auto">62 {Array.from({ length: 8 }).map((_, i) => (63 <TileSkeleton key={i} />64 ))}65 </div>66 </div>67 ))}68 </div>69 );70}7172async function Categories() {73 const markets = await getMarketRows();74 const families = markets.filter((r) => r.counts.assets > 0);75 const [thumbs, sparklines] = await Promise.all([getCategoryThumbnails(markets.map((r) => r.node.slug)), getCategorySparklines(markets.map((r) => r.node.slug), 90)]);76 return (77 <section>78 <div className="mb-3 flex items-end justify-between gap-3">79 <div>80 <h2 className="t-title text-fg">Browse by category</h2>81 <p className="mt-0.5 text-xs text-muted">Live counts from the canonical tables · 90-day category index where published</p>82 </div>83 <Link href="/categories" className="num shrink-0 text-xs font-medium text-muted hover:text-fg">84 {fmtNum(families.length)} of {fmtNum(markets.length)} families with data →85 </Link>86 </div>87 <CategoryGrid rows={families} thumbs={thumbs} sparklines={sparklines} limit={12} mobileLimit={6} />88 </section>89 );90}9192async function FlagshipPanel() {93 const indices = await listIndices();94 const rare = indices.find((i) => i.isFlagship);95 const subs = indices.filter((i) => !i.isFlagship);96 const published = subs.filter((i) => i.latest);97 const building = subs.filter((i) => !i.latest).sort((a, b) => b.pricedAssets / b.minConstituents - a.pricedAssets / a.minConstituents);98 const series = rare ? await getIndexSeries(rare.id, 365) : [];99 return (100 <section className="grid gap-4 lg:grid-cols-[3fr_2fr]">101 <Card className="p-4">102 <div className="flex flex-wrap items-baseline justify-between gap-3">103 <div>104 <p className="t-label">RARE · Global Collectibles Index</p>105 {rare?.latest ? (106 <p className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5">107 <span className="num text-2xl font-semibold text-fg">{rare.latest.value.toFixed(2)}</span>108 <Delta value={rare.change1d} arrow className="text-sm" />109 <span className="text-[11px] text-subtle">1D</span>110 <Delta value={rare.change30d} className="text-sm" />111 <span className="text-[11px] text-subtle">30D</span>112 </p>113 ) : (114 <p className="mt-1 text-sm text-muted">Transaction-weighted across published subindices · base 1,000 on 2024-01-01 · <Link href="/methodology#indices" className="underline-offset-4 hover:text-fg hover:underline">methodology</Link></p>115 )}116 </div>117 <Link href="/rareindex/RARE" className="text-xs font-medium text-muted hover:text-fg">118 Index detail →119 </Link>120 </div>121 <div className="mt-3">122 <LineChart ariaLabel="RARE index over the last year" height={220} series={[{ id: 'rare', label: 'RARE', kind: 'area', points: series.map((p) => ({ x: p.date, y: p.value })) }]} emptyLabel={rare ? `Index building — publishes once ${rare.minConstituents} priced constituents exist (${rare.pricedAssets} today)` : 'Index not configured'} />123 </div>124 </Card>125 <Card className="flex flex-col p-4">126 <div className="flex items-baseline justify-between">127 <p className="t-label">Subindices</p>128 <Link href="/rareindex" className="text-xs font-medium text-muted hover:text-fg">129 All indices →130 </Link>131 </div>132 {published.length ? (133 <ul className="mt-2 divide-y divide-border">134 {published.slice(0, 8).map((i) => (135 <li key={i.ticker}>136 <Link href={`/rareindex/${i.ticker}`} className="flex min-h-[40px] items-center justify-between gap-3 text-[12px] hover:bg-sunken">137 <span className="flex min-w-0 items-center gap-2">138 <span className="inline-block h-2 w-2 shrink-0 rounded-[2px]" style={{ background: i.color ?? 'var(--ri-index)' }} />139 <span className="mono-num font-semibold text-fg">{i.ticker}</span>140 <span className="truncate text-muted">{i.name.replace('RareIndex ', '')}</span>141 </span>142 <span className="num flex shrink-0 items-center gap-3">143 <span className="text-muted">{i.latest!.value.toFixed(2)}</span>144 <Delta value={i.change1d} arrow />145 </span>146 </Link>147 </li>148 ))}149 </ul>150 ) : null}151 {building.length ? (152 <div className="mt-3 border-t border-border pt-3">153 <p className="t-caption mb-1.5 flex items-baseline justify-between"><span>In development · priced constituents vs. minimum</span><Link href="/methodology#indices" className="text-subtle hover:text-fg">Why →</Link></p>154 <ul className="grid grid-cols-2 gap-x-4 gap-y-1.5">155 {building.slice(0, 8).map((i) => {156 const pct = Math.min(100, Math.round((i.pricedAssets / Math.max(1, i.minConstituents)) * 100));157 return (158 <li key={i.ticker} className="text-[11px]">159 <span className="flex items-center justify-between">160 <span className="mono-num text-muted">{i.ticker.replace('RARE-', '')}</span>161 <span className="num text-subtle">162 {i.pricedAssets}/{i.minConstituents}163 </span>164 </span>165 <span className="mt-0.5 block h-1 w-full overflow-hidden rounded-full bg-inset">166 <span className="block h-full rounded-full bg-index" style={{ width: `${pct}%` }} />167 </span>168 </li>169 );170 })}171 </ul>172 </div>173 ) : null}174 </Card>175 </section>176 );177}178179/** Market snapshot (§195): the category table right under the global index. */180async function Snapshot() {181 const markets = await getMarketRows();182 return <MarketInsights rows={markets} />;183}184185/** Long tail below the fold: catalogue additions and community signals. */186async function LongTail() {187 const [newest, watched] = await Promise.all([rankedAssets('newest', { limit: 8 }), rankedAssets('watched', { limit: 8 })]);188 await attachGuidePrices(newest);189 return (190 <>191 <AssetRail title="Newly Added" subtitle="Latest canonical assets — guide prices labelled, RIV when available" href="/explore?sort=newest" items={newest} metric="guide" />192 {watched.length ? <AssetRail title="Most Watched" subtitle="Assets on the most watchlists" href="/explore?sort=trending" items={watched} metric="watchers" /> : null}193 </>194 );195}196197async function Discovery() {198 const [trending, movers, losers, records, radar, lots, opportunities, newestPriced, documented, withSales] = await Promise.all([199 rankedAssets('trending', { limit: 8 }),200 rankedAssets('gainers', { limit: 8, window: '7d' }),201 rankedAssets('losers', { limit: 8, window: '7d' }),202 getTopSales(8),203 listRadar({ limit: 8 }),204 listLots({ endingWithinHours: 72, status: null, limit: 8 }),205 rankedAssets('opportunity', { limit: 8 }),206 rankedAssets('newest_priced', { limit: 8 }),207 rankedAssets('documented', { limit: 8 }),208 rankedAssets('with_sales', { limit: 8 }),209 ]);210 await Promise.all([attachGuidePrices(documented), attachGuidePrices(trending), attachGuidePrices(newestPriced)]);211 // Honest fallbacks while valuations are still being computed: each rail states what it shows.212 const trendingRail = trending.length213 ? { title: 'Trending Now', subtitle: 'Price × volume × listing momentum', items: trending, metric: 'trending' as const, href: '/trending' }214 : newestPriced.length215 ? { title: 'Recently valued', subtitle: 'Newest RareIndex Valuations — trending publishes once momentum history exists', items: newestPriced, metric: 'riv' as const, href: '/explore?has=valuation&sort=riv' }216 : { title: 'Best-documented assets', subtitle: 'Images plus sales or guide prices', items: documented, metric: 'guide' as const, href: '/explore?has=observations' };217 const moversItems = movers.length ? movers : losers;218 const moversRail = moversItems.length219 ? { title: 'Biggest Movers', subtitle: '7-day change in RareIndex Valuation', items: moversItems, metric: 'change7d' as const, href: '/explore?sort=change7d' }220 : withSales.length221 ? { title: 'Latest sold assets', subtitle: 'Most recent verified sale per asset — movers need a 7-day valuation history', items: withSales, metric: 'latestSale' as const, href: '/sales' }222 : null;223 return (224 <>225 <AssetRail title={trendingRail.title} subtitle={trendingRail.subtitle} href={trendingRail.href} items={trendingRail.items} metric={trendingRail.metric} priority />226 {moversRail ? <AssetRail title={moversRail.title} subtitle={moversRail.subtitle} href={moversRail.href} items={moversRail.items} metric={moversRail.metric} /> : null}227 <SalesRail title="Record Sales" subtitle="Highest verified transactions" href="/records" items={records} hideWhenEmpty />228 <RadarRail items={radar} hideWhenEmpty />229 {opportunities.length ? <AssetRail title="Value Opportunities" subtitle="Asks ≥ 10 % below a transaction-based RIV of the same variant (≥ 5 sales, medium+ confidence). Analytical data, not advice; implausible asks are held back as data anomalies." href="/listings?sort=discount&disc=10" items={opportunities} metric="opportunity" /> : null}230 <LotsRail items={lots} hideWhenEmpty />231 </>232 );233}234235async function Latest() {236 const items = await getLatestSales(12);237 const observations = items.length ? [] : await getLatestObservationsFeed(12);238 return <LatestTransactions items={items} observations={observations} />;239}240