SPB Git

spb/rareindex

Public
8.9 KB · 169 lines tsx
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
Files · d93392862678cbf30e175697fcb812bc4ff0b5a7
Menu

Theme