TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Suspense } from 'react';4import { FAMILIES } from '@rareindex/taxonomy';5import { PageHeader } from '@/components/ui/page-header';6import { Card, CardHeader, EmptyState, Table, th, td, tdNum, Badge, StatStrip } from '@/components/ui/primitives';7import { Tabs } from '@/components/ui/tabs';8import { FilterBar } from '@/components/ui/filter-bar';9import { Pagination } from '@/components/ui/pagination';10import { LotsIntelCards, LotsIntelTable } from '@/components/market/lots-table';11import { getLotOverview, listAuctionHouses, listAuctions, listLotsPaged, type LotSort } from '@/lib/queries/market-lists';12import { feeScheduleFor } from '@rareindex/valuation';13import { feeScheduleSummary, FeeConfidenceBadge } from '@/components/market/fee-schedule';14import { houseSlug, ENDING_WINDOWS, endingHours } from '@/lib/auction-house';15import { fmtDate, fmtNum, fmtRelative, cn } from '@/lib/format';16import { catName } from '@/lib/taxonomy';17import { sp1, spEnum, spInt, type SP } from '@/lib/search-params';1819export const metadata: Metadata = { title: 'Auctions', description: 'Live and upcoming collectible auction lots with the buyer-pays all-in cost, the RareIndex Valuation and bid-vs-RIV, across auction houses worldwide.' };2021const TABS = ['open', 'ending', 'below', 'results', 'houses'] as const;2223export default async function AuctionsPage({ searchParams }: { searchParams: Promise<SP> }) {24 const sp = await searchParams;25 const tab = spEnum(sp, 'tab', TABS, 'open');26 const category = sp1(sp, 'category') ?? null;27 const house = sp1(sp, 'house') ?? null;28 const ending = sp1(sp, 'ending') ?? null;29 const below = sp1(sp, 'below') === '1' || tab === 'below';30 const sort = spEnum(sp, 'sort', ['ending', 'value', 'discount', 'bids'] as const, tab === 'below' ? 'discount' : 'ending') as LotSort;31 const page = spInt(sp, 'page');32 const hours = tab === 'ending' ? (endingHours(ending) ?? 72) : endingHours(ending);3334 const [overview, houses, auctions, lots] = await Promise.all([35 getLotOverview(),36 listAuctionHouses(),37 tab === 'houses' ? Promise.resolve([]) : listAuctions({ limit: 40, house }),38 tab === 'houses'39 ? Promise.resolve({ items: [], total: 0, page: 1, pageSize: 50 })40 : listLotsPaged({41 open: tab !== 'results',42 status: tab === 'results' ? 'ended' : null,43 endingWithinHours: hours,44 category,45 house,46 maxVsRiv: below ? -0.1 : null,47 sort: tab === 'results' ? (sort === 'ending' ? 'value' : sort) : sort,48 page,49 pageSize: 50,50 }),51 ]);5253 const params = { tab, category: category ?? undefined, house: house ?? undefined, ending: ending ?? undefined, below: below && tab !== 'below' ? '1' : undefined, sort: sort !== 'ending' ? sort : undefined };54 const q = (extra: Record<string, string | undefined>) => {55 const u = new URLSearchParams();56 for (const [k, v] of Object.entries({ ...params, ...extra })) if (v) u.set(k, v);57 const s = u.toString();58 return `/auctions${s ? `?${s}` : ''}`;59 };6061 return (62 <div>63 <PageHeader64 kicker="Auction intelligence"65 title="Auctions"66 description="Live and upcoming lots from auction-house connectors with the buyer-pays cost and the RareIndex Valuation side by side. All-in = hammer + buyer's premium from the house's published or estimated schedule; VAT/duties/shipping not included. Model analytics, not advice."67 compact68 actions={69 <Link href="/auctions/calendar" className="rounded-md border border-border px-2.5 py-1.5 text-xs font-medium hover:bg-inset">70 Auction calendar →71 </Link>72 }73 />74 <StatStrip75 className="mb-4"76 items={[77 { label: 'Open lots', value: fmtNum(overview.open), sub: 'live + upcoming' },78 { label: 'Ending 24 h', value: fmtNum(overview.ending24h), href: q({ tab: 'ending', ending: '24h' }) },79 { label: 'With bids', value: fmtNum(overview.withBids) },80 { label: 'Below RIV', value: fmtNum(overview.belowRiv), sub: overview.assessed ? `of ${fmtNum(overview.assessed)} assessed` : 'not assessed yet', href: q({ tab: 'below' }) },81 { label: 'Houses', value: fmtNum(overview.houses), href: q({ tab: 'houses' }) },82 ]}83 />84 <Tabs85 active={tab}86 tabs={[87 { id: 'open', label: 'Open lots', href: q({ tab: 'open' }) },88 { id: 'ending', label: 'Ending soon', href: q({ tab: 'ending' }) },89 { id: 'below', label: 'Below RIV', count: overview.belowRiv || null, href: q({ tab: 'below' }) },90 { id: 'results', label: 'Results', href: q({ tab: 'results' }) },91 { id: 'houses', label: 'Auction houses', count: houses.length, href: q({ tab: 'houses' }) },92 ]}93 className="mb-4"94 />9596 {tab === 'houses' ? (97 <Card className="overflow-hidden">98 <CardHeader title="Auction houses" subtitle="Sources with auction connectors · buyer's premium schedule on file when known" />99 {houses.length ? (100 <Table>101 <thead>102 <tr>103 <th className={th}>House</th>104 <th className={th}>Buyer's premium</th>105 <th className={cn(th, 'text-right')}>Auctions</th>106 <th className={cn(th, 'text-right')}>Lots tracked</th>107 <th className={cn(th, 'text-right')}>Open</th>108 </tr>109 </thead>110 <tbody>111 {houses.map((h) => {112 const s = feeScheduleFor(h.house);113 return (114 <tr key={h.house} className="hover:bg-sunken">115 <td className={cn(td, 'font-medium')}>116 <Link href={`/auctions/house/${houseSlug(h.house)}`} className="hover:underline">117 {h.house}118 </Link>119 </td>120 <td className={cn(td, 'text-muted')}>121 {s ? (122 <span className="inline-flex flex-wrap items-center gap-1.5">123 <span className="num">{feeScheduleSummary(s)}</span>124 <FeeConfidenceBadge confidence={s.confidence} />125 </span>126 ) : (127 <FeeConfidenceBadge confidence="default" />128 )}129 </td>130 <td className={tdNum}>{fmtNum(h.auctions)}</td>131 <td className={tdNum}>{fmtNum(h.lots)}</td>132 <td className={tdNum}>{fmtNum(h.upcoming)}</td>133 </tr>134 );135 })}136 </tbody>137 </Table>138 ) : (139 <EmptyState title="No auction houses connected yet" description="Auction-house connectors (results and catalogues) are part of the connector roadmap." />140 )}141 </Card>142 ) : (143 <>144 <div className="mb-3 flex flex-wrap items-center justify-between gap-2">145 <Suspense>146 <FilterBar147 resetKeys={['page']}148 fields={[149 { name: 'category', label: 'Category', type: 'select', options: FAMILIES.map((f) => ({ value: f.slug, label: f.name })), width: 'w-44' },150 { name: 'house', label: 'House', type: 'select', options: houses.map((h) => ({ value: h.house, label: h.house })), width: 'w-44' },151 ...(tab === 'results' ? [] : [{ name: 'ending', label: 'Ending within', type: 'select' as const, options: ENDING_WINDOWS.map((w) => ({ value: w.id, label: w.label })), placeholder: tab === 'ending' ? '72 h' : 'Any', width: 'w-32' }]),152 { name: 'sort', label: 'Sort', type: 'select', options: [{ value: 'ending', label: 'Ending soonest' }, { value: 'discount', label: 'Most below RIV' }, { value: 'bids', label: 'Most bids' }, { value: 'value', label: 'Highest value' }], placeholder: tab === 'below' ? 'Most below RIV' : 'Ending soonest', width: 'w-40' },153 ...(tab === 'below' || tab === 'results' ? [] : [{ name: 'below', label: 'Below RIV only', type: 'select' as const, options: [{ value: '1', label: 'Yes' }], placeholder: 'No', width: 'w-32' }]),154 ]}155 />156 </Suspense>157 <p className="text-[11px] text-subtle">158 {fmtNum(lots.total)} lot{lots.total === 1 ? '' : 's'}159 {tab === 'ending' ? ` · ending within ${hours} h` : ''}160 {below ? ' · assessed all-in bid or estimate ≥ 10 % below the RIV of the same variant' : ''}161 </p>162 </div>163 <div className="grid gap-4 xl:grid-cols-[3fr_1fr]">164 <Card className="overflow-hidden">165 <CardHeader166 title={tab === 'ending' ? `Lots ending in the next ${hours} hours` : tab === 'below' ? 'Lots below the RareIndex Valuation' : tab === 'results' ? 'Recorded results' : 'Open lots'}167 subtitle={tab === 'below' ? 'Gated exactly like listings: transaction-based RIV of the same variant, ≥ 5 sales, medium+ confidence; opening prices without a bid use the low estimate (“est.”). Deeper than −50 % is held for review.' : 'Estimates and bids are the house’s own figures; all-in adds the buyer’s premium.'}168 />169 <div className="md:hidden">170 <LotsIntelCards items={lots.items} />171 {!lots.items.length ? <EmptyState title="No lots" description={tab === 'below' ? 'No assessed lot is below its RIV right now — the auctions worker assesses open lots hourly.' : 'Lots appear once auction-house connectors publish catalogues.'} /> : null}172 </div>173 <div className="hidden md:block">174 <LotsIntelTable items={lots.items} emptyDescription={tab === 'below' ? 'No assessed lot is below its RIV right now — the auctions worker assesses open lots hourly.' : undefined} />175 </div>176 <Pagination page={lots.page} pageSize={lots.pageSize} total={lots.total} basePath="/auctions" params={params} className="border-t border-border px-4 py-3" />177 </Card>178 <Card className="overflow-hidden">179 <CardHeader title="Auctions" subtitle="Catalogues tracked" />180 {auctions.length ? (181 <ul className="divide-y divide-border text-[12px]">182 {auctions.map((a) => (183 <li key={a.id} className="px-4 py-2">184 <a href={a.url} target="_blank" rel="noopener nofollow" className="font-medium text-fg hover:underline">185 {a.name}186 </a>187 <div className="mt-0.5 flex flex-wrap items-center gap-1.5 text-[11px] text-muted">188 <Badge tone={a.status === 'live' ? 'gain' : a.status === 'upcoming' ? 'index' : 'neutral'}>{a.status}</Badge>189 <Link href={`/auctions/house/${houseSlug(a.auctionHouse)}`} className="hover:text-fg">190 {a.auctionHouse}191 </Link>192 <span>· {a.endsAt ? `ends ${fmtDate(a.endsAt)} (${fmtRelative(a.endsAt)})` : a.startsAt ? `starts ${fmtDate(a.startsAt)}` : 'dates n/a'}</span>193 <span>· {a.lotsTracked} lots{a.lotCount ? ` / ${a.lotCount}` : ''}</span>194 {a.categorySlugs.slice(0, 2).map((c) => (195 <span key={c}>· {catName(c)}</span>196 ))}197 </div>198 </li>199 ))}200 </ul>201 ) : (202 <EmptyState title="No auctions tracked" className="py-8" />203 )}204 </Card>205 </div>206 </>207 )}208 </div>209 );210}211