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 { attachGuidePrices, getCategoryThumbnails, getLatestObservationsFeed } from '@/lib/queries/assets';8import { fmtNum } from '@/lib/format';9import { Skeleton } from '@/components/ui/primitives';10import { getLatestSales, getSiteStats } from '@/lib/queries/site';11import { listIndices } from '@/lib/queries/indices';12import { rankedAssets } from '@/lib/queries/assets';13import { getMarketRows } from '@/lib/queries/markets';14import { getTopSales, listLots, listRadar } from '@/lib/queries/market-lists';15import { LineChart } from '@/components/charts/line-chart';16import { getIndexSeries } from '@/lib/queries/indices';1718export const revalidate = 120;1920export default async function HomePage() {21 const [stats, indices] = await Promise.all([getSiteStats(), listIndices()]);22 return (23 <div className="-mx-4 sm:-mx-6">24 <div className="px-4 sm:px-6">25 <Hero stats={stats} />26 </div>27 <div className="mt-8">28 <IndexTape indices={indices} />29 </div>30 <div className="px-4 pt-6 sm:px-6">31 <Suspense fallback={<Skeleton className="h-64 w-full" />}>32 <FlagshipPanel />33 </Suspense>34 <Suspense fallback={<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">{Array.from({ length: 6 }).map((_, i) => <Skeleton key={i} className="h-72" />)}</div>}>35 <Discovery />36 </Suspense>37 <Suspense fallback={<Skeleton className="mt-4 h-48" />}>38 <Latest />39 </Suspense>40 </div>41 </div>42 );43}4445async function FlagshipPanel() {46 const indices = await listIndices();47 const rare = indices.find((i) => i.isFlagship);48 const subs = indices.filter((i) => !i.isFlagship && i.latest);49 const series = rare ? await getIndexSeries(rare.id, 365) : [];50 return (51 <section className="grid gap-4 lg:grid-cols-[2fr_1fr]">52 <div className="card p-4">53 <div className="flex items-baseline justify-between gap-3">54 <div>55 <h2 className="text-sm font-semibold text-fg">RARE · RareIndex Global Collectibles Index</h2>56 <p className="text-xs text-muted">Chain-linked, equal-weighted across subindices · base 1,000 on 2024-01-01</p>57 </div>58 <Link href="/rareindex/RARE" className="text-xs font-medium text-muted hover:text-fg">59 Index detail →60 </Link>61 </div>62 <div className="mt-3">63 <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'} />64 </div>65 </div>66 <div className="card p-4">67 <h2 className="text-sm font-semibold text-fg">Subindices</h2>68 <p className="text-xs text-muted">Daily change · latest published value</p>69 {subs.length ? (70 <ul className="mt-2 divide-y divide-border">71 {subs.slice(0, 10).map((i) => (72 <li key={i.ticker}>73 <Link href={`/rareindex/${i.ticker}`} className="flex items-center justify-between py-1.5 text-[12px] hover:bg-sunken">74 <span className="flex items-center gap-2">75 <span className="inline-block h-2 w-2 rounded-full" style={{ background: i.color ?? 'var(--ri-index)' }} />76 <span className="font-medium text-fg">{i.ticker}</span>77 </span>78 <span className="num flex items-center gap-3">79 <span className="text-muted">{i.latest!.value.toFixed(2)}</span>80 <span className={i.change1d === null ? 'text-subtle' : i.change1d >= 0 ? 'text-gain' : 'text-loss'}>{i.change1d === null ? '—' : `${i.change1d >= 0 ? '+' : ''}${(i.change1d * 100).toFixed(2)}%`}</span>81 </span>82 </Link>83 </li>84 ))}85 </ul>86 ) : (87 <p className="mt-6 text-center text-xs text-subtle">88 No subindex has reached its minimum constituent count yet.89 <br />90 <Link href="/rareindex" className="underline hover:text-fg">91 See build progress92 </Link>93 </p>94 )}95 </div>96 </section>97 );98}99100async function Discovery() {101 const [trending, movers, losers, records, radar, lots, watched, newest, markets, opportunities, newestPriced, documented, withSales] = await Promise.all([102 rankedAssets('trending', { limit: 8 }),103 rankedAssets('gainers', { limit: 8, window: '7d' }),104 rankedAssets('losers', { limit: 8, window: '7d' }),105 getTopSales(8),106 listRadar({ limit: 8 }),107 listLots({ endingWithinHours: 72, status: null, limit: 8 }),108 rankedAssets('watched', { limit: 8 }),109 rankedAssets('newest', { limit: 8 }),110 getMarketRows(),111 rankedAssets('opportunity', { limit: 8 }),112 rankedAssets('newest_priced', { limit: 8 }),113 rankedAssets('documented', { limit: 8 }),114 rankedAssets('with_sales', { limit: 8 }),115 ]);116 await Promise.all([attachGuidePrices(newest), attachGuidePrices(documented)]);117 const families = markets.filter((r) => r.counts.assets > 0);118 const thumbs = await getCategoryThumbnails(markets.map((r) => r.node.slug));119 // Honest fallbacks while valuations are still being computed: each rail states what it shows.120 const trendingRail = trending.length121 ? { title: 'Trending Now', subtitle: 'Price × volume × listing momentum', items: trending, metric: 'trending' as const, href: '/trending' }122 : newestPriced.length123 ? { title: 'Recently valued', subtitle: 'Newest RareIndex Valuations (trending needs momentum history)', items: newestPriced, metric: 'riv' as const, href: '/explore?has=valuation&sort=riv' }124 : { title: 'Best-documented assets', subtitle: 'Images plus sales or guide prices — trending publishes once momentum history exists', items: documented, metric: 'guide' as const, href: '/explore?has=observations' };125 const moversItems = movers.length ? movers : losers;126 const moversRail = moversItems.length127 ? { title: 'Biggest Movers', subtitle: '7-day change in RareIndex Valuation', items: moversItems, metric: 'change7d' as const, href: '/explore?sort=change7d' }128 : withSales.length129 ? { title: 'Latest sold assets', subtitle: 'Most recent verified sale per asset — movers need a 7-day valuation history', items: withSales, metric: 'riv' as const, href: '/sales' }130 : null;131 return (132 <>133 <section className="mt-6">134 <div className="mb-2 flex items-baseline justify-between">135 <h2 className="text-base font-semibold tracking-tight text-fg">Browse by category</h2>136 <span className="num text-[11px] text-muted">137 {fmtNum(families.length)} families with data ·{' '}138 <Link href="/categories" className="hover:text-fg">139 all {fmtNum(markets.length)} →140 </Link>141 </span>142 </div>143 <CategoryGrid rows={markets} thumbs={thumbs} limit={12} />144 </section>145 <section className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">146 <AssetRail title={trendingRail.title} subtitle={trendingRail.subtitle} href={trendingRail.href} items={trendingRail.items} metric={trendingRail.metric} />147 {moversRail ? <AssetRail title={moversRail.title} subtitle={moversRail.subtitle} href={moversRail.href} items={moversRail.items} metric={moversRail.metric} /> : null}148 <SalesRail title="Record Sales" subtitle="Highest verified transactions" href="/records" items={records} hideWhenEmpty />149 <RadarRail items={radar} hideWhenEmpty />150 <LotsRail items={lots} hideWhenEmpty />151 <AssetRail title="Newly Added" subtitle="Latest canonical assets — guide prices labelled, RIV when available" href="/explore?sort=newest" items={newest} metric="guide" />152 <MarketInsights rows={markets} />153 {watched.length ? <AssetRail title="Most Watched" subtitle="Assets on the most watchlists" href="/explore?sort=trending" items={watched} metric="watchers" /> : null}154 {opportunities.length ? <AssetRail title="Value Opportunities" subtitle="Listings materially below RIV (analytical, not advice)" href="/listings?sort=discount" items={opportunities} metric="opportunity" /> : null}155 </section>156 </>157 );158}159160async function Latest() {161 const items = await getLatestSales(12);162 const observations = items.length ? [] : await getLatestObservationsFeed(12);163 return (164 <section className="mt-6">165 <LatestTransactions items={items} observations={observations} />166 </section>167 );168}169