SPB Git

spb/rareindex

Public
9.4 KB · 186 lines tsx
1import Link from 'next/link';2import type { AssetCard as AssetCardData, SaleRow } from '@/lib/queries/assets';3import type { RadarRow, LotRow } from '@/lib/queries/market-lists';4import type { LatestSale } from '@/lib/queries/site';5import type { MarketRow } from '@/lib/queries/markets';6import { fmtMoney, fmtRelative, fmtNum, cn, fmtDate } from '@/lib/format';7import { catName, humanize } from '@/lib/taxonomy';8import { Card, CardHeader, Delta, EmptyState } from '@/components/ui/primitives';9import { GradeBadge, SourceLink, Thumb } from '@/components/market/bits';10import { heatStyle } from '@/components/charts/heat';1112function Row({ href, image, title, sub, right, rightSub }: { href: string; image?: string | null; title: string; sub?: string | null; right: React.ReactNode; rightSub?: React.ReactNode }) {13  return (14    <li>15      <Link href={href} className="flex items-center gap-2.5 px-4 py-2 hover:bg-sunken">16        <Thumb src={image} alt="" size={32} />17        <span className="min-w-0 flex-1">18          <span className="block truncate text-[13px] font-medium text-fg">{title}</span>19          {sub ? <span className="block truncate text-[11px] text-muted">{sub}</span> : null}20        </span>21        <span className="num flex shrink-0 flex-col items-end text-[12px] leading-tight">22          <span className="font-medium text-fg">{right}</span>23          {rightSub ? <span className="text-[11px]">{rightSub}</span> : null}24        </span>25      </Link>26    </li>27  );28}2930const EMPTY = 'Populates as the pipeline ingests data.';3132export function AssetRail({ title, subtitle, href, items, metric, empty = EMPTY }: { title: string; subtitle?: string; href: string; items: AssetCardData[]; metric: 'change7d' | 'change1d' | 'change30d' | 'trending' | 'watchers' | 'riv' | 'opportunity' | 'new'; empty?: string }) {33  return (34    <Card>35      <CardHeader title={title} subtitle={subtitle} action={<Link href={href} className="text-muted hover:text-fg">View all →</Link>} />36      {items.length ? (37        <ul className="divide-y divide-border">38          {items.map((a) => (39            <Row40              key={a.id}41              href={`/asset/${a.slug}`}42              image={a.heroImageUrl}43              title={a.title}44              sub={`${catName(a.categorySlug)}${a.year ? ` · ${a.year}` : ''}`}45              right={a.rivUsd === null ? <span className="text-subtle">—</span> : fmtMoney(a.rivUsd)}46              rightSub={47                metric === 'trending' ? <span className="text-muted">score {a.trendingScore?.toFixed(1) ?? '—'}</span>48                : metric === 'watchers' ? <span className="text-muted">{fmtNum(a.watchers)} watching</span>49                : metric === 'new' ? <span className="text-muted">added {fmtRelative(a.updatedAt)}</span>50                : metric === 'riv' ? <span className="text-muted">{a.rivSampleSize} sales</span>51                : metric === 'opportunity' ? <Delta value={a.valueOpportunity === null ? null : -a.valueOpportunity} />52                : <Delta value={metric === 'change1d' ? a.change1d : metric === 'change30d' ? a.change30d : a.change7d} />53              }54            />55          ))}56        </ul>57      ) : (58        <EmptyState title="Nothing to show yet" description={empty} className="py-8" />59      )}60    </Card>61  );62}6364export function SalesRail({ title, subtitle, href, items }: { title: string; subtitle?: string; href: string; items: SaleRow[] }) {65  return (66    <Card>67      <CardHeader title={title} subtitle={subtitle} action={<Link href={href} className="text-muted hover:text-fg">View all →</Link>} />68      {items.length ? (69        <ul className="divide-y divide-border">70          {items.map((s) => (71            <Row key={s.id} href={s.assetSlug ? `/asset/${s.assetSlug}` : '/sales'} image={s.heroImageUrl ?? s.imageUrls[0]} title={s.assetTitle ?? s.rawTitle} sub={`${s.sourceName} · ${fmtDate(s.saleDate)}`} right={fmtMoney(s.priceUsd)} rightSub={<GradeBadge grader={s.grader} grade={s.grade} />} />72          ))}73        </ul>74      ) : (75        <EmptyState title="No verified sales yet" description="Record sales are drawn from verified transactions only (§154)." className="py-8" />76      )}77    </Card>78  );79}8081export function RadarRail({ items }: { items: RadarRow[] }) {82  return (83    <Card>84      <CardHeader title="Rare Finds" subtitle="Rare Radar: unusual appearances and discrepancies" action={<Link href="/radar" className="text-muted hover:text-fg">Open radar →</Link>} />85      {items.length ? (86        <ul className="divide-y divide-border">87          {items.map((r) => (88            <Row key={r.id} href={`/asset/${r.assetSlug}`} image={r.heroImageUrl} title={r.assetTitle} sub={`${humanize(r.kind)} · ${fmtRelative(r.detectedAt)}`} right={r.rivUsd === null ? <span className="text-subtle">—</span> : fmtMoney(r.rivUsd)} rightSub={<span className="text-muted">signal {r.score.toFixed(0)}</span>} />89          ))}90        </ul>91      ) : (92        <EmptyState title="Radar is quiet" description="Findings appear when the pipeline detects first listings in years, ultra-low populations or price discrepancies." className="py-8" />93      )}94    </Card>95  );96}9798export function LotsRail({ items }: { items: LotRow[] }) {99  return (100    <Card>101      <CardHeader title="Auctions Ending Soon" subtitle="Next 72 hours" action={<Link href="/auctions" className="text-muted hover:text-fg">All auctions →</Link>} />102      {items.length ? (103        <ul className="divide-y divide-border">104          {items.map((l) => (105            <li key={l.id}>106              <a href={l.url} target="_blank" rel="noopener nofollow" className="flex items-center gap-2.5 px-4 py-2 hover:bg-sunken">107                <Thumb src={l.imageUrls[0]} alt="" size={32} />108                <span className="min-w-0 flex-1">109                  <span className="block truncate text-[13px] font-medium text-fg">{l.title}</span>110                  <span className="block truncate text-[11px] text-muted">111                    {l.auctionHouse} · ends {fmtRelative(l.endsAt)}112                  </span>113                </span>114                <span className="num text-[12px] font-medium text-fg">{l.currentBid !== null ? fmtMoney(l.currentBid, l.currency ?? 'USD') : l.estimateHigh !== null ? `est. ${fmtMoney(l.estimateHigh, l.currency ?? 'USD')}` : '—'}</span>115              </a>116            </li>117          ))}118        </ul>119      ) : (120        <EmptyState title="No lots ending soon" description="Auction lots appear once auction-house connectors are live." className="py-8" />121      )}122    </Card>123  );124}125126export function MarketInsights({ rows }: { rows: MarketRow[] }) {127  const withData = rows.filter((r) => r.counts.assets > 0);128  return (129    <Card>130      <CardHeader title="Market Insights" subtitle="Category performance and activity" action={<Link href="/markets" className="text-muted hover:text-fg">Markets →</Link>} />131      {withData.length ? (132        <ul className="divide-y divide-border">133          {withData.slice(0, 10).map((r) => (134            <li key={r.node.slug}>135              <Link href={`/markets/${r.node.slug}`} className="grid grid-cols-[1fr_auto_auto] items-center gap-3 px-4 py-2 text-[12px] hover:bg-sunken">136                <span className="truncate font-medium text-fg">{r.node.name}</span>137                <span className="num text-muted">{fmtNum(r.counts.assets)} assets · {fmtNum(r.counts.sales)} sales</span>138                <span className="num rounded-sm px-1.5 py-0.5" style={heatStyle(r.snapshot?.change30d)}>139                  <Delta value={r.snapshot?.change30d} />140                </span>141              </Link>142            </li>143          ))}144        </ul>145      ) : (146        <EmptyState title="No category data yet" description={EMPTY} className="py-8" />147      )}148    </Card>149  );150}151152export function LatestTransactions({ items }: { items: LatestSale[] }) {153  return (154    <Card>155      <CardHeader title="Latest transactions" subtitle="Most recent verified sales across sources" action={<Link href="/sales" className="text-muted hover:text-fg">All sales →</Link>} />156      {items.length ? (157        <ul className="grid grid-cols-1 divide-y divide-border md:grid-cols-2 md:divide-y-0 lg:grid-cols-3">158          {items.map((s) => (159            <li key={s.id} className="border-b border-border md:[&:nth-child(2n)]:border-l lg:[&:nth-child(2n)]:border-l-0 lg:[&:nth-child(3n+2)]:border-l lg:[&:nth-child(3n)]:border-l">160              <Link href={`/asset/${s.assetSlug}`} className="flex items-center gap-2.5 px-4 py-2.5 hover:bg-sunken">161                <Thumb src={s.heroImageUrl} alt="" size={36} />162                <span className="min-w-0 flex-1">163                  <span className="block truncate text-[13px] font-medium text-fg">{s.assetTitle}</span>164                  <span className="flex items-center gap-1.5 text-[11px] text-muted">165                    <GradeBadge grader={s.grader} grade={s.grade} />166                    <span className="truncate">167                      {fmtDate(s.saleDate)} · <SourceLink name={s.sourceName} url={null} />168                    </span>169                  </span>170                </span>171                <span className="num text-[13px] font-semibold text-fg">{fmtMoney(s.priceUsd)}</span>172              </Link>173            </li>174          ))}175        </ul>176      ) : (177        <EmptyState title="No transactions yet" description="Verified sales stream in here as connectors run." className="py-8" />178      )}179    </Card>180  );181}182183export function cls(...c: Array<string | false | undefined>): string {184  return cn(...c);185}186
Files · f9a634ec80792524177fc3e5fba5c7953d672423
Menu

Theme