TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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