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