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