SPB Git

spb/rareindex

Public
11.2 KB · 220 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 { 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      <div className="flex flex-col gap-10 px-4 pt-8 sm:px-6">30        <Suspense fallback={<Skeleton className="h-40" />}>31          <Categories />32        </Suspense>33        <Suspense fallback={<RailFallback />}>34          <Discovery />35        </Suspense>36        <Suspense fallback={<Skeleton className="h-72 w-full" />}>37          <FlagshipPanel />38        </Suspense>39        <Suspense fallback={<TableSkeleton rows={10} />}>40          <Latest />41        </Suspense>42      </div>43    </div>44  );45}4647function RailFallback() {48  return (49    <div className="flex flex-col gap-10">50      {[0, 1].map((k) => (51        <div key={k}>52          <Skeleton className="mb-3 h-5 w-40" />53          <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">54            {Array.from({ length: 8 }).map((_, i) => (55              <TileSkeleton key={i} />56            ))}57          </div>58        </div>59      ))}60    </div>61  );62}6364async function Categories() {65  const markets = await getMarketRows();66  const families = markets.filter((r) => r.counts.assets > 0);67  const [thumbs, sparklines] = await Promise.all([getCategoryThumbnails(markets.map((r) => r.node.slug)), getCategorySparklines(markets.map((r) => r.node.slug), 90)]);68  return (69    <section>70      <div className="mb-3 flex items-end justify-between gap-3">71        <div>72          <h2 className="t-title text-fg">Browse by category</h2>73          <p className="mt-0.5 text-xs text-muted">Live counts from the canonical tables · 90-day category index where published</p>74        </div>75        <Link href="/categories" className="num shrink-0 text-xs font-medium text-muted hover:text-fg">76          {fmtNum(families.length)} of {fmtNum(markets.length)} families with data →77        </Link>78      </div>79      <CategoryGrid rows={families} thumbs={thumbs} sparklines={sparklines} limit={12} mobileLimit={6} />80    </section>81  );82}8384async function FlagshipPanel() {85  const indices = await listIndices();86  const rare = indices.find((i) => i.isFlagship);87  const subs = indices.filter((i) => !i.isFlagship);88  const published = subs.filter((i) => i.latest);89  const building = subs.filter((i) => !i.latest).sort((a, b) => b.pricedAssets / b.minConstituents - a.pricedAssets / a.minConstituents);90  const series = rare ? await getIndexSeries(rare.id, 365) : [];91  return (92    <section className="grid gap-4 lg:grid-cols-[3fr_2fr]">93      <Card className="p-4">94        <div className="flex flex-wrap items-baseline justify-between gap-3">95          <div>96            <p className="t-label">RARE · Global Collectibles Index</p>97            {rare?.latest ? (98              <p className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5">99                <span className="num text-2xl font-semibold text-fg">{rare.latest.value.toFixed(2)}</span>100                <Delta value={rare.change1d} arrow className="text-sm" />101                <span className="text-[11px] text-subtle">1D</span>102                <Delta value={rare.change30d} className="text-sm" />103                <span className="text-[11px] text-subtle">30D</span>104              </p>105            ) : (106              <p className="mt-1 text-sm text-muted">Chain-linked, equal-weighted across subindices · base 1,000 on 2024-01-01</p>107            )}108          </div>109          <Link href="/rareindex/RARE" className="text-xs font-medium text-muted hover:text-fg">110            Index detail →111          </Link>112        </div>113        <div className="mt-3">114          <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'} />115        </div>116      </Card>117      <Card className="flex flex-col p-4">118        <div className="flex items-baseline justify-between">119          <p className="t-label">Subindices</p>120          <Link href="/rareindex" className="text-xs font-medium text-muted hover:text-fg">121            All indices →122          </Link>123        </div>124        {published.length ? (125          <ul className="mt-2 divide-y divide-border">126            {published.slice(0, 8).map((i) => (127              <li key={i.ticker}>128                <Link href={`/rareindex/${i.ticker}`} className="flex min-h-[40px] items-center justify-between gap-3 text-[12px] hover:bg-sunken">129                  <span className="flex min-w-0 items-center gap-2">130                    <span className="inline-block h-2 w-2 shrink-0 rounded-[2px]" style={{ background: i.color ?? 'var(--ri-index)' }} />131                    <span className="mono-num font-semibold text-fg">{i.ticker}</span>132                    <span className="truncate text-muted">{i.name.replace('RareIndex ', '')}</span>133                  </span>134                  <span className="num flex shrink-0 items-center gap-3">135                    <span className="text-muted">{i.latest!.value.toFixed(2)}</span>136                    <Delta value={i.change1d} arrow />137                  </span>138                </Link>139              </li>140            ))}141          </ul>142        ) : null}143        {building.length ? (144          <div className="mt-3 border-t border-border pt-3">145            <p className="t-caption mb-1.5">Building · priced constituents vs. minimum</p>146            <ul className="grid grid-cols-2 gap-x-4 gap-y-1.5">147              {building.slice(0, 8).map((i) => {148                const pct = Math.min(100, Math.round((i.pricedAssets / Math.max(1, i.minConstituents)) * 100));149                return (150                  <li key={i.ticker} className="text-[11px]">151                    <span className="flex items-center justify-between">152                      <span className="mono-num text-muted">{i.ticker.replace('RARE-', '')}</span>153                      <span className="num text-subtle">154                        {i.pricedAssets}/{i.minConstituents}155                      </span>156                    </span>157                    <span className="mt-0.5 block h-1 w-full overflow-hidden rounded-full bg-inset">158                      <span className="block h-full rounded-full bg-index" style={{ width: `${pct}%` }} />159                    </span>160                  </li>161                );162              })}163            </ul>164          </div>165        ) : null}166      </Card>167    </section>168  );169}170171async function Discovery() {172  const [trending, movers, losers, records, radar, lots, watched, newest, markets, opportunities, newestPriced, documented, withSales] = await Promise.all([173    rankedAssets('trending', { limit: 8 }),174    rankedAssets('gainers', { limit: 8, window: '7d' }),175    rankedAssets('losers', { limit: 8, window: '7d' }),176    getTopSales(8),177    listRadar({ limit: 8 }),178    listLots({ endingWithinHours: 72, status: null, limit: 8 }),179    rankedAssets('watched', { limit: 8 }),180    rankedAssets('newest', { limit: 8 }),181    getMarketRows(),182    rankedAssets('opportunity', { limit: 8 }),183    rankedAssets('newest_priced', { limit: 8 }),184    rankedAssets('documented', { limit: 8 }),185    rankedAssets('with_sales', { limit: 8 }),186  ]);187  await Promise.all([attachGuidePrices(newest), attachGuidePrices(documented), attachGuidePrices(trending), attachGuidePrices(newestPriced)]);188  // Honest fallbacks while valuations are still being computed: each rail states what it shows.189  const trendingRail = trending.length190    ? { title: 'Trending Now', subtitle: 'Price × volume × listing momentum', items: trending, metric: 'trending' as const, href: '/trending' }191    : newestPriced.length192      ? { 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' }193      : { title: 'Best-documented assets', subtitle: 'Images plus sales or guide prices', items: documented, metric: 'guide' as const, href: '/explore?has=observations' };194  const moversItems = movers.length ? movers : losers;195  const moversRail = moversItems.length196    ? { title: 'Biggest Movers', subtitle: '7-day change in RareIndex Valuation', items: moversItems, metric: 'change7d' as const, href: '/explore?sort=change7d' }197    : withSales.length198      ? { 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' }199      : null;200  return (201    <>202      <AssetRail title={trendingRail.title} subtitle={trendingRail.subtitle} href={trendingRail.href} items={trendingRail.items} metric={trendingRail.metric} priority />203      {moversRail ? <AssetRail title={moversRail.title} subtitle={moversRail.subtitle} href={moversRail.href} items={moversRail.items} metric={moversRail.metric} /> : null}204      <SalesRail title="Record Sales" subtitle="Highest verified transactions" href="/records" items={records} hideWhenEmpty />205      <RadarRail items={radar} hideWhenEmpty />206      <LotsRail items={lots} hideWhenEmpty />207      <AssetRail title="Newly Added" subtitle="Latest canonical assets — guide prices labelled, RIV when available" href="/explore?sort=newest" items={newest} metric="guide" />208      {opportunities.length ? <AssetRail title="Value Opportunities" subtitle="Listings materially below RIV — analytical data, not advice" href="/listings?sort=discount" items={opportunities} metric="opportunity" /> : null}209      {watched.length ? <AssetRail title="Most Watched" subtitle="Assets on the most watchlists" href="/explore?sort=trending" items={watched} metric="watchers" /> : null}210      <MarketInsights rows={markets} />211    </>212  );213}214215async function Latest() {216  const items = await getLatestSales(12);217  const observations = items.length ? [] : await getLatestObservationsFeed(12);218  return <LatestTransactions items={items} observations={observations} />;219}220
Files · d732f70ce5489245a263ca6bcf09fbebc0d2be77
Menu

Theme