SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.1 KB · 77 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader } from '@/components/ui/page-header';4import { Card, CardHeader, Delta, EmptyState } from '@/components/ui/primitives';5import { AssetList } from '@/components/market/asset-list';6import { rankedAssets, categoryScope } from '@/lib/queries/assets';7import { getTrendingCategories } from '@/lib/queries/market-lists';8import { catName } from '@/lib/taxonomy';9import { sp1, type SP } from '@/lib/search-params';10import { FAMILIES } from '@rareindex/taxonomy';11import { fmtNum } from '@/lib/format';1213export const metadata: Metadata = { title: 'Trending collectibles', description: 'What collectors are moving into: trending score from price, volume, listing, search and news momentum.' };14export const revalidate = 120;1516export default async function TrendingPage({ searchParams }: { searchParams: Promise<SP> }) {17  const sp = await searchParams;18  const category = sp1(sp, 'category') ?? null;19  const scope = category ? categoryScope(category) : undefined;20  const [trending, cats, gainers7, gainers30] = await Promise.all([rankedAssets('trending', { scope, limit: 60 }), getTrendingCategories(12), rankedAssets('gainers', { scope, limit: 12, window: '7d' }), rankedAssets('gainers', { scope, limit: 12, window: '30d' })]);21  return (22    <div>23      <PageHeader kicker="Momentum"24        title="Trending"25        description="Trending = price momentum × volume momentum × search momentum × listing momentum × news momentum, each normalised (§152). Scores are computed by the pipeline from observed data only."26        meta={27          <>28            <Link href="/trending" className={!category ? 'font-medium text-fg' : 'hover:text-fg'}>29              All30            </Link>31            {FAMILIES.filter((f) => f.phase === 1).map((f) => (32              <Link key={f.slug} href={`/trending?category=${f.slug}`} className={category === f.slug ? 'font-medium text-fg' : 'hover:text-fg'}>33                {f.name}34              </Link>35            ))}36          </>37        }38      />39      <section className="grid gap-4 lg:grid-cols-[3fr_1fr]">40        <Card className="overflow-hidden">41          <CardHeader title="Trending assets" subtitle="Ranked by trending score; momentum breakdown per horizon" />42          <AssetList items={trending} rank columns={['riv', 'trending', 'change1d', 'change7d', 'change30d', 'sales30d', 'listings', 'liquidity']} metric="trending" emptyTitle="No trending scores yet" emptyDescription="Trending needs several days of valuations, sales and listing history." />43        </Card>44        <Card>45          <CardHeader title="Trending categories" subtitle="Average trending score" />46          {cats.length ? (47            <ul className="divide-y divide-border text-[12px]">48              {cats.map((c) => (49                <li key={c.categorySlug}>50                  <Link href={`/markets/${c.categorySlug}`} className="flex items-center justify-between px-4 py-2 hover:bg-sunken">51                    <span className="font-medium text-fg">{catName(c.categorySlug)}</span>52                    <span className="num flex items-center gap-2 text-muted">53                      {c.trending.toFixed(1)} · {fmtNum(c.assets)} <Delta value={c.momentum} />54                    </span>55                  </Link>56                </li>57              ))}58            </ul>59          ) : (60            <EmptyState title="No data yet" className="py-8" />61          )}62        </Card>63      </section>64      <section className="mt-4 grid gap-4 lg:grid-cols-2">65        <Card className="overflow-hidden">66          <CardHeader title="Top gainers · 7D" />67          <AssetList items={gainers7} columns={['riv', 'change7d', 'sales30d']} emptyTitle="No 7-day movers" emptyDescription="Requires ≥3 sales per asset and a week of history." />68        </Card>69        <Card className="overflow-hidden">70          <CardHeader title="Top gainers · 30D" />71          <AssetList items={gainers30} columns={['riv', 'change30d', 'sales30d']} emptyTitle="No 30-day movers" emptyDescription="Requires ≥3 sales per asset and a month of history." />72        </Card>73      </section>74    </div>75  );76}77