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.5 KB · 75 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import { Suspense } from 'react';3import { PageHeader } from '@/components/ui/page-header';4import { Segmented } from '@/components/ui/tabs';5import { FilterBar } from '@/components/ui/filter-bar';6import { Card, EmptyState, Badge } from '@/components/ui/primitives';7import { listAuctionHouses, listAuctions } from '@/lib/queries/market-lists';8import { fmtDate, fmtRelative } from '@/lib/format';9import { catName } from '@/lib/taxonomy';10import { FAMILIES } from '@rareindex/taxonomy';11import { sp1, spEnum, withParams, type SP } from '@/lib/search-params';1213export const metadata: Metadata = { title: 'Auction calendar', description: 'Upcoming collectible auctions today, this week and this month, filterable by category, house and region.' };1415export default async function CalendarPage({ searchParams }: { searchParams: Promise<SP> }) {16  const sp = await searchParams;17  const range = spEnum(sp, 'range', ['today', 'week', 'month'] as const, 'week');18  const category = sp1(sp, 'category') ?? null;19  const house = sp1(sp, 'house') ?? null;20  const now = new Date();21  const to = new Date(now.getTime() + (range === 'today' ? 1 : range === 'week' ? 7 : 31) * 86_400_000);22  const [auctions, houses] = await Promise.all([listAuctions({ from: now, to, category, house, limit: 200 }), listAuctionHouses()]);23  const byDay = new Map<string, typeof auctions>();24  for (const a of auctions) {25    const d = (a.endsAt ?? a.startsAt ?? now).toISOString().slice(0, 10);26    (byDay.get(d) ?? byDay.set(d, []).get(d)!).push(a);27  }28  const params = { category: category ?? undefined, house: house ?? undefined };29  return (30    <div>31      <PageHeader kicker="Auction houses" crumbs={[{ label: 'Auctions', href: '/auctions' }, { label: 'Calendar' }]} title="Auction calendar" description="Auctions closing in the selected window, grouped by day (UTC)." compact />32      <div className="mb-4 flex flex-wrap items-end justify-between gap-3">33        <Segmented active={range} options={[{ id: 'today', label: 'Today', href: withParams('/auctions/calendar', { ...params, range: 'today' }) }, { id: 'week', label: 'This week', href: withParams('/auctions/calendar', { ...params, range: 'week' }) }, { id: 'month', label: 'This month', href: withParams('/auctions/calendar', { ...params, range: 'month' }) }]} />34        <Suspense>35          <FilterBar fields={[{ name: 'category', label: 'Category', type: 'select', options: FAMILIES.map((f) => ({ value: f.slug, label: f.name })), width: 'w-44' }, { name: 'house', label: 'House', type: 'select', options: houses.map((h) => ({ value: h.house, label: h.house })), width: 'w-44' }]} />36        </Suspense>37      </div>38      {byDay.size ? (39        <div className="space-y-4">40          {[...byDay.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([day, items]) => (41            <Card key={day}>42              <div className="border-b border-border px-4 py-2 text-xs font-semibold">{fmtDate(day, { month: 'long' })}</div>43              <ul className="divide-y divide-border text-[12px]">44                {items.map((a) => (45                  <li key={a.id} className="flex flex-wrap items-center justify-between gap-2 px-4 py-2">46                    <div className="min-w-0">47                      <a href={a.url} target="_blank" rel="noopener nofollow" className="font-medium text-fg hover:underline">48                        {a.name}49                      </a>50                      <div className="text-[11px] text-muted">51                        {a.auctionHouse}52                        {a.location ? ` · ${a.location}` : ''}53                        {a.categorySlugs.length ? ` · ${a.categorySlugs.slice(0, 3).map(catName).join(', ')}` : ''}54                      </div>55                    </div>56                    <div className="flex items-center gap-2 text-[11px] text-muted">57                      <Badge tone={a.status === 'live' ? 'gain' : 'index'}>{a.status}</Badge>58                      <span>{a.endsAt ? `closes ${fmtDate(a.endsAt, { time: true })} (${fmtRelative(a.endsAt)})` : a.startsAt ? `starts ${fmtDate(a.startsAt, { time: true })}` : ''}</span>59                      <span>· {a.lotsTracked} lots</span>60                    </div>61                  </li>62                ))}63              </ul>64            </Card>65          ))}66        </div>67      ) : (68        <Card>69          <EmptyState title="No auctions in this window" description="Auctions appear once auction-house connectors publish catalogues. Try a wider range." />70        </Card>71      )}72    </div>73  );74}75