SPB Git

spb/rareindex

Public
6.4 KB · 132 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 { 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
Files · d92b8b3d34ca4c272753d94b3a3ee1ff6e902121
Menu

Theme