SPB Git

spb/rareindex

Public
11.8 KB · 225 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' | 'guide'; 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 ? (a.guideUsd != null ? <span className="text-muted" title="Price-guide observation, not a transaction">{fmtMoney(a.guideUsd)}</span> : <span className="text-subtle">—</span>) : fmtMoney(a.rivUsd)}46              rightSub={47                metric === 'guide' ? <span className="text-muted">{a.rivUsd === null && a.guideUsd != null ? `guide · ${a.guideSource ?? ''}` : a.rivUsd !== null ? `RIV · ${a.rivSampleSize} sales` : 'catalogued'}</span>48                : metric === 'trending' ? <span className="text-muted">score {a.trendingScore?.toFixed(1) ?? '—'}</span>49                : metric === 'watchers' ? <span className="text-muted">{fmtNum(a.watchers)} watching</span>50                : metric === 'new' ? <span className="text-muted">added {fmtRelative(a.updatedAt)}</span>51                : metric === 'riv' ? <span className="text-muted">{a.rivSampleSize} sales</span>52                : metric === 'opportunity' ? <Delta value={a.valueOpportunity === null ? null : -a.valueOpportunity} />53                : <Delta value={metric === 'change1d' ? a.change1d : metric === 'change30d' ? a.change30d : a.change7d} />54              }55            />56          ))}57        </ul>58      ) : (59        <EmptyState title="Nothing to show yet" description={empty} className="py-8" />60      )}61    </Card>62  );63}6465export function SalesRail({ title, subtitle, href, items, hideWhenEmpty = false }: { title: string; subtitle?: string; href: string; items: SaleRow[]; hideWhenEmpty?: boolean }) {66  if (hideWhenEmpty && !items.length) return null;67  return (68    <Card>69      <CardHeader title={title} subtitle={subtitle} action={<Link href={href} className="text-muted hover:text-fg">View all →</Link>} />70      {items.length ? (71        <ul className="divide-y divide-border">72          {items.map((s) => (73            <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} />} />74          ))}75        </ul>76      ) : (77        <EmptyState title="No verified sales yet" description="Record sales are drawn from verified transactions only (§154)." className="py-8" />78      )}79    </Card>80  );81}8283export function RadarRail({ items, hideWhenEmpty = false }: { items: RadarRow[]; hideWhenEmpty?: boolean }) {84  if (hideWhenEmpty && !items.length) return null;85  return (86    <Card>87      <CardHeader title="Rare Finds" subtitle="Rare Radar: unusual appearances and discrepancies" action={<Link href="/radar" className="text-muted hover:text-fg">Open radar →</Link>} />88      {items.length ? (89        <ul className="divide-y divide-border">90          {items.map((r) => (91            <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>} />92          ))}93        </ul>94      ) : (95        <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" />96      )}97    </Card>98  );99}100101export function LotsRail({ items, hideWhenEmpty = false }: { items: LotRow[]; hideWhenEmpty?: boolean }) {102  if (hideWhenEmpty && !items.length) return null;103  return (104    <Card>105      <CardHeader title="Auctions Ending Soon" subtitle="Next 72 hours" action={<Link href="/auctions" className="text-muted hover:text-fg">All auctions →</Link>} />106      {items.length ? (107        <ul className="divide-y divide-border">108          {items.map((l) => (109            <li key={l.id}>110              <a href={l.url} target="_blank" rel="noopener nofollow" className="flex items-center gap-2.5 px-4 py-2 hover:bg-sunken">111                <Thumb src={l.imageUrls[0]} alt="" size={32} />112                <span className="min-w-0 flex-1">113                  <span className="block truncate text-[13px] font-medium text-fg">{l.title}</span>114                  <span className="block truncate text-[11px] text-muted">115                    {l.auctionHouse} · ends {fmtRelative(l.endsAt)}116                  </span>117                </span>118                <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>119              </a>120            </li>121          ))}122        </ul>123      ) : (124        <EmptyState title="No lots ending soon" description="Auction lots appear once auction-house connectors are live." className="py-8" />125      )}126    </Card>127  );128}129130export function MarketInsights({ rows }: { rows: MarketRow[] }) {131  const withData = rows.filter((r) => r.counts.assets > 0);132  return (133    <Card>134      <CardHeader title="Market Insights" subtitle="Category performance and activity" action={<Link href="/markets" className="text-muted hover:text-fg">Markets →</Link>} />135      {withData.length ? (136        <ul className="divide-y divide-border">137          {withData.slice(0, 10).map((r) => (138            <li key={r.node.slug}>139              <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">140                <span className="truncate font-medium text-fg">{r.node.name}</span>141                <span className="num text-muted">{fmtNum(r.counts.assets)} assets · {fmtNum(r.counts.sales)} sales</span>142                <span className="num rounded-sm px-1.5 py-0.5" style={heatStyle(r.snapshot?.change30d)}>143                  <Delta value={r.snapshot?.change30d} />144                </span>145              </Link>146            </li>147          ))}148        </ul>149      ) : (150        <EmptyState title="No category data yet" description={EMPTY} className="py-8" />151      )}152    </Card>153  );154}155156export interface LatestObservation {157  id: string;158  assetSlug: string;159  assetTitle: string;160  categorySlug: string;161  heroImageUrl: string | null;162  priceUsd: number;163  priceKind: string;164  observationDate: string;165  sourceName: string;166}167168export function LatestTransactions({ items, observations = [] }: { items: LatestSale[]; observations?: LatestObservation[] }) {169  if (!items.length && observations.length) {170    return (171      <Card>172        <CardHeader title="Latest guide price observations" subtitle="No verified sales indexed yet — these are price-guide observations (not transactions), shown with their source and date" action={<Link href="/explore?has=observations" className="text-muted hover:text-fg">Browse →</Link>} />173        <ul className="grid grid-cols-1 divide-y divide-border md:grid-cols-2 md:divide-y-0 lg:grid-cols-3">174          {observations.map((o) => (175            <li key={o.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">176              <Link href={`/asset/${o.assetSlug}`} className="flex items-center gap-2.5 px-4 py-2.5 hover:bg-sunken">177                <Thumb src={o.heroImageUrl} alt="" size={36} />178                <span className="min-w-0 flex-1">179                  <span className="block truncate text-[13px] font-medium text-fg">{o.assetTitle}</span>180                  <span className="block truncate text-[11px] text-muted">181                    {catName(o.categorySlug)} · {o.sourceName} · {humanize(o.priceKind)} · {fmtDate(o.observationDate)}182                  </span>183                </span>184                <span className="num text-[13px] font-semibold text-muted">{fmtMoney(o.priceUsd)}</span>185              </Link>186            </li>187          ))}188        </ul>189      </Card>190    );191  }192  return (193    <Card>194      <CardHeader title="Latest transactions" subtitle="Most recent verified sales across sources" action={<Link href="/sales" className="text-muted hover:text-fg">All sales →</Link>} />195      {items.length ? (196        <ul className="grid grid-cols-1 divide-y divide-border md:grid-cols-2 md:divide-y-0 lg:grid-cols-3">197          {items.map((s) => (198            <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">199              <Link href={`/asset/${s.assetSlug}`} className="flex items-center gap-2.5 px-4 py-2.5 hover:bg-sunken">200                <Thumb src={s.heroImageUrl} alt="" size={36} />201                <span className="min-w-0 flex-1">202                  <span className="block truncate text-[13px] font-medium text-fg">{s.assetTitle}</span>203                  <span className="flex items-center gap-1.5 text-[11px] text-muted">204                    <GradeBadge grader={s.grader} grade={s.grade} />205                    <span className="truncate">206                      {fmtDate(s.saleDate)} · <SourceLink name={s.sourceName} url={null} />207                    </span>208                  </span>209                </span>210                <span className="num text-[13px] font-semibold text-fg">{fmtMoney(s.priceUsd)}</span>211              </Link>212            </li>213          ))}214        </ul>215      ) : (216        <EmptyState title="No transactions yet" description="Verified sales stream in here as connectors run." className="py-8" />217      )}218    </Card>219  );220}221222export function cls(...c: Array<string | false | undefined>): string {223  return cn(...c);224}225
Files · d93392862678cbf30e175697fcb812bc4ff0b5a7
Menu

Theme