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 { Skeleton } from '@/components/ui/primitives';7import { getLatestSales, getSiteStats } from '@/lib/queries/site';8import { listIndices } from '@/lib/queries/indices';9import { rankedAssets } from '@/lib/queries/assets';10import { getMarketRows } from '@/lib/queries/markets';11import { getTopSales, listLots, listRadar } from '@/lib/queries/market-lists';12import { LineChart } from '@/components/charts/line-chart';13import { getIndexSeries } from '@/lib/queries/indices';1415export const revalidate = 120;1617export default async function HomePage() {18 const [stats, indices] = await Promise.all([getSiteStats(), listIndices()]);19 return (20 <div className="-mx-4 sm:-mx-6">21 <div className="px-4 sm:px-6">22 <Hero stats={stats} />23 </div>24 <div className="mt-8">25 <IndexTape indices={indices} />26 </div>27 <div className="px-4 pt-6 sm:px-6">28 <Suspense fallback={<Skeleton className="h-64 w-full" />}>29 <FlagshipPanel />30 </Suspense>31 <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>}>32 <Discovery />33 </Suspense>34 <Suspense fallback={<Skeleton className="mt-4 h-48" />}>35 <Latest />36 </Suspense>37 </div>38 </div>39 );40}4142async function FlagshipPanel() {43 const indices = await listIndices();44 const rare = indices.find((i) => i.isFlagship);45 const subs = indices.filter((i) => !i.isFlagship && i.latest);46 const series = rare ? await getIndexSeries(rare.id, 365) : [];47 return (48 <section className="grid gap-4 lg:grid-cols-[2fr_1fr]">49 <div className="card p-4">50 <div className="flex items-baseline justify-between gap-3">51 <div>52 <h2 className="text-sm font-semibold text-fg">RARE · RareIndex Global Collectibles Index</h2>53 <p className="text-xs text-muted">Chain-linked, equal-weighted across subindices · base 1,000 on 2024-01-01</p>54 </div>55 <Link href="/rareindex/RARE" className="text-xs font-medium text-muted hover:text-fg">56 Index detail →57 </Link>58 </div>59 <div className="mt-3">60 <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'} />61 </div>62 </div>63 <div className="card p-4">64 <h2 className="text-sm font-semibold text-fg">Subindices</h2>65 <p className="text-xs text-muted">Daily change · latest published value</p>66 {subs.length ? (67 <ul className="mt-2 divide-y divide-border">68 {subs.slice(0, 10).map((i) => (69 <li key={i.ticker}>70 <Link href={`/rareindex/${i.ticker}`} className="flex items-center justify-between py-1.5 text-[12px] hover:bg-sunken">71 <span className="flex items-center gap-2">72 <span className="inline-block h-2 w-2 rounded-full" style={{ background: i.color ?? 'var(--ri-index)' }} />73 <span className="font-medium text-fg">{i.ticker}</span>74 </span>75 <span className="num flex items-center gap-3">76 <span className="text-muted">{i.latest!.value.toFixed(2)}</span>77 <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>78 </span>79 </Link>80 </li>81 ))}82 </ul>83 ) : (84 <p className="mt-6 text-center text-xs text-subtle">85 No subindex has reached its minimum constituent count yet.86 <br />87 <Link href="/rareindex" className="underline hover:text-fg">88 See build progress89 </Link>90 </p>91 )}92 </div>93 </section>94 );95}9697async function Discovery() {98 const [trending, movers, losers, records, radar, lots, watched, newest, markets] = await Promise.all([99 rankedAssets('trending', { limit: 8 }),100 rankedAssets('gainers', { limit: 8, window: '7d' }),101 rankedAssets('losers', { limit: 8, window: '7d' }),102 getTopSales(8),103 listRadar({ limit: 8 }),104 listLots({ endingWithinHours: 72, status: null, limit: 8 }),105 rankedAssets('watched', { limit: 8 }),106 rankedAssets('newest', { limit: 8 }),107 getMarketRows(),108 ]);109 return (110 <section className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">111 <AssetRail title="Trending Now" subtitle="Price × volume × listing momentum" href="/trending" items={trending} metric="trending" />112 <AssetRail title="Biggest Movers" subtitle="7-day change in RareIndex Valuation" href="/explore?sort=change7d" items={movers.length ? movers : losers} metric="change7d" empty="Movers appear when assets have ≥3 sales and a 7-day history." />113 <SalesRail title="Record Sales" subtitle="Highest verified transactions" href="/records" items={records} />114 <RadarRail items={radar} />115 <LotsRail items={lots} />116 <AssetRail title="Most Watched" subtitle="Assets on the most watchlists" href="/explore?sort=trending" items={watched} metric="watchers" empty="Watch an asset from its page to see it here." />117 <AssetRail title="Newly Added" subtitle="Latest canonical assets" href="/explore?sort=newest" items={newest} metric="new" />118 <MarketInsights rows={markets} />119 <AssetRail title="Value Opportunities" subtitle="Listings materially below RIV (analytical, not advice)" href="/listings?sort=discount" items={await rankedAssets('opportunity', { limit: 8 })} metric="opportunity" empty="Requires valuations and active listings." />120 </section>121 );122}123124async function Latest() {125 const items = await getLatestSales(12);126 return (127 <section className="mt-6">128 <LatestTransactions items={items} />129 </section>130 );131}132