SPB Git

spb/rareindex

Public
5.4 KB · 110 lines tsx
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader } from '@/components/ui/page-header';4import { Card, CardHeader, EmptyState, Table, th, td, tdNum, Badge } from '@/components/ui/primitives';5import { Tabs } from '@/components/ui/tabs';6import { LotsTable } from '@/components/market/sales-table';7import { listAuctionHouses, listAuctions, listLots } from '@/lib/queries/market-lists';8import { fmtDate, fmtNum, fmtRelative, cn } from '@/lib/format';9import { catName } from '@/lib/taxonomy';10import { spEnum, type SP } from '@/lib/search-params';1112export const metadata: Metadata = { title: 'Auctions', description: 'Auction houses, live and upcoming lots, ending-soon lots and results across the collectibles market.' };1314export default async function AuctionsPage({ searchParams }: { searchParams: Promise<SP> }) {15  const sp = await searchParams;16  const tab = spEnum(sp, 'tab', ['ending', 'live', 'upcoming', 'results', 'houses'] as const, 'ending');17  const [houses, ending, lots, auctions] = await Promise.all([18    listAuctionHouses(),19    listLots({ endingWithinHours: 72, limit: 60 }),20    tab === 'results' ? listLots({ status: 'ended', limit: 60, sort: 'value' }) : tab === 'live' ? listLots({ status: 'live', limit: 60 }) : tab === 'upcoming' ? listLots({ status: 'upcoming', limit: 60 }) : Promise.resolve([]),21    listAuctions({ limit: 100 }),22  ]);23  return (24    <div>25      <PageHeader kicker="Auction houses"26        title="Auctions"27        description="Lots aggregated from auction-house connectors. Estimates and bids are the house’s own figures; hammer prices flow into Sales once an auction ends and the lot is matched to a canonical asset."28        compact29        actions={30          <Link href="/auctions/calendar" className="rounded-md border border-border px-2.5 py-1.5 text-xs font-medium hover:bg-inset">31            Auction calendar →32          </Link>33        }34      />35      <Tabs36        active={tab}37        tabs={[38          { id: 'ending', label: 'Ending soon', count: ending.length, href: '/auctions?tab=ending' },39          { id: 'live', label: 'Live', href: '/auctions?tab=live' },40          { id: 'upcoming', label: 'Upcoming', href: '/auctions?tab=upcoming' },41          { id: 'results', label: 'Results', href: '/auctions?tab=results' },42          { id: 'houses', label: 'Auction houses', count: houses.length, href: '/auctions?tab=houses' },43        ]}44        className="mb-4"45      />46      {tab === 'houses' ? (47        <Card className="overflow-hidden">48          <CardHeader title="Auction houses" subtitle="Sources with auction connectors" />49          {houses.length ? (50            <Table>51              <thead>52                <tr>53                  <th className={th}>House</th>54                  <th className={cn(th, 'text-right')}>Auctions</th>55                  <th className={cn(th, 'text-right')}>Lots tracked</th>56                  <th className={cn(th, 'text-right')}>Open</th>57                </tr>58              </thead>59              <tbody>60                {houses.map((h) => (61                  <tr key={h.house}>62                    <td className={cn(td, 'font-medium')}>{h.house}</td>63                    <td className={tdNum}>{fmtNum(h.auctions)}</td>64                    <td className={tdNum}>{fmtNum(h.lots)}</td>65                    <td className={tdNum}>{fmtNum(h.upcoming)}</td>66                  </tr>67                ))}68              </tbody>69            </Table>70          ) : (71            <EmptyState title="No auction houses connected yet" description="Auction-house connectors (results and catalogues) are part of the Phase 1 connector roadmap." />72          )}73        </Card>74      ) : (75        <div className="grid gap-4 xl:grid-cols-[2fr_1fr]">76          <Card className="overflow-hidden">77            <CardHeader title={tab === 'ending' ? 'Lots ending in the next 72 hours' : tab === 'results' ? 'Recent results by value' : `${tab[0]!.toUpperCase()}${tab.slice(1)} lots`} />78            <LotsTable items={tab === 'ending' ? ending : lots} />79          </Card>80          <Card className="overflow-hidden">81            <CardHeader title="Auctions" subtitle="Catalogues tracked" />82            {auctions.length ? (83              <ul className="divide-y divide-border text-[12px]">84                {auctions.slice(0, 40).map((a) => (85                  <li key={a.id} className="px-4 py-2">86                    <a href={a.url} target="_blank" rel="noopener nofollow" className="font-medium text-fg hover:underline">87                      {a.name}88                    </a>89                    <div className="mt-0.5 flex flex-wrap items-center gap-1.5 text-[11px] text-muted">90                      <Badge tone={a.status === 'live' ? 'gain' : a.status === 'upcoming' ? 'index' : 'neutral'}>{a.status}</Badge>91                      <span>{a.auctionHouse}</span>92                      <span>· {a.endsAt ? `ends ${fmtDate(a.endsAt)} (${fmtRelative(a.endsAt)})` : a.startsAt ? `starts ${fmtDate(a.startsAt)}` : 'dates n/a'}</span>93                      <span>· {a.lotsTracked} lots{a.lotCount ? ` / ${a.lotCount}` : ''}</span>94                      {a.categorySlugs.slice(0, 2).map((c) => (95                        <span key={c}>· {catName(c)}</span>96                      ))}97                    </div>98                  </li>99                ))}100              </ul>101            ) : (102              <EmptyState title="No auctions tracked" className="py-8" />103            )}104          </Card>105        </div>106      )}107    </div>108  );109}110
Files · d732f70ce5489245a263ca6bcf09fbebc0d2be77
Menu

Theme