1import Image from 'next/image';2import Link from 'next/link';3import type { AssetDetail, VariantRow } from '@/lib/queries/assets';4import { fmtMoney, fmtNum, fmtRelative, fmtDate, cn, confidenceLabel } from '@/lib/format';5import { catName, categoryCrumbs } from '@/lib/taxonomy';6import { Breadcrumbs } from '@/components/ui/page-header';7import { Badge, Delta, Stat } from '@/components/ui/primitives';8import { Evidence, ScoreMeter } from '@/components/ui/evidence';910export function AssetHeader({ asset, variants, activeVariant, tabHref }: { asset: AssetDetail; variants: VariantRow[]; activeVariant: VariantRow | null; tabHref: (v: string | null) => string }) {11 const v = activeVariant;12 const riv = v ? v.rivUsd : asset.rivUsd;13 const low = v ? v.rivLowUsd : asset.rivLowUsd;14 const high = v ? v.rivHighUsd : asset.rivHighUsd;15 const conf = v ? v.rivConfidence : asset.rivConfidence;16 const n = v ? v.rivSampleSize : asset.rivSampleSize;17 const latest = v ? v.latestSaleUsd : asset.latestSaleUsd;18 const latestAt = v ? v.latestSaleAt : asset.latestSaleAt;19 return (20 <header>21 <Breadcrumbs items={[{ label: 'Markets', href: '/markets' }, ...categoryCrumbs(asset.categorySlug), ...(asset.setSlug && asset.setName ? [{ label: asset.setName, href: `/set/${asset.setSlug}` }] : []), { label: asset.name }]} />22 <div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_320px]">23 <div className="flex gap-4">24 <div className="relative h-28 w-28 shrink-0 overflow-hidden rounded-md border border-border bg-inset sm:h-40 sm:w-40">25 {asset.heroImageUrl ? <Image src={asset.heroImageUrl} alt={asset.title} fill sizes="160px" className="object-contain" priority /> : <span className="absolute inset-0 flex items-center justify-center text-[10px] uppercase tracking-wider text-subtle">No image</span>}26 </div>27 <div className="min-w-0">28 <h1 className="text-xl font-semibold tracking-tight text-fg sm:text-2xl">{asset.title}</h1>29 <div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px]">30 <Link href={`/markets/${asset.categorySlug}`}>31 <Badge tone="index">{catName(asset.categorySlug)}</Badge>32 </Link>33 {asset.year ? <Badge>{asset.year}</Badge> : null}34 {asset.edition ? <Badge>{asset.edition}</Badge> : null}35 {asset.variant ? <Badge>{asset.variant}</Badge> : null}36 {asset.language && asset.language !== 'en' ? <Badge>{asset.language.toUpperCase()}</Badge> : null}37 {asset.rarity ? <Badge tone="rarity">{asset.rarity}</Badge> : null}38 {asset.verified ? <Badge tone="gain">Verified record</Badge> : null}39 </div>40 <dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-1 text-[12px] sm:grid-cols-3">41 {[42 ['Set', asset.setName ? <Link key="s" href={asset.setSlug ? `/set/${asset.setSlug}` : '#'} className="hover:underline">{asset.setName}{asset.setCode ? ` (${asset.setCode})` : ''}</Link> : null],43 ['Number', asset.number],44 ['Brand', asset.brand ? <Link key="b" href={`/brand/${encodeURIComponent(asset.brand)}`} className="hover:underline">{asset.brand}</Link> : null],45 ['Franchise', asset.franchise],46 ['Reference', asset.reference],47 ['Model', asset.model],48 ['Release', asset.releaseDate],49 ['Production', asset.productionQuantity != null ? fmtNum(asset.productionQuantity) : null],50 ['Original MSRP', asset.originalMsrp != null ? fmtMoney(asset.originalMsrp, asset.originalMsrpCurrency ?? 'USD') : null],51 ]52 .filter(([, val]) => val)53 .map(([k, val]) => (54 <div key={k as string} className="min-w-0">55 <dt className="text-[10px] uppercase tracking-wider text-subtle">{k}</dt>56 <dd className="truncate text-fg">{val}</dd>57 </div>58 ))}59 </dl>60 {variants.length ? (61 <div className="mt-3">62 <p className="mb-1 text-[10px] font-medium uppercase tracking-wider text-subtle">Variant / grade</p>63 <div className="flex flex-wrap gap-1">64 <Link href={tabHref(null)} scroll={false} className={cn('rounded-md border px-2 py-1 text-[11px]', !v ? 'border-fg bg-accent text-accent-fg' : 'border-border text-muted hover:text-fg')}>65 All variants66 </Link>67 {variants.slice(0, 12).map((x) => (68 <Link key={x.id} href={tabHref(x.id)} scroll={false} className={cn('rounded-md border px-2 py-1 text-[11px]', v?.id === x.id ? 'border-fg bg-accent text-accent-fg' : 'border-border text-muted hover:text-fg')} title={`${x.salesCount} sales`}>69 {x.label}70 {x.salesCount ? <span className="num ml-1 opacity-70">{x.salesCount}</span> : null}71 </Link>72 ))}73 </div>74 </div>75 ) : null}76 </div>77 </div>78 <aside className="card p-4">79 <div className="flex items-baseline justify-between">80 <span className="text-[10px] font-semibold uppercase tracking-wider text-subtle">RareIndex Valuation{v ? ` · ${v.label}` : ''}</span>81 <span className="text-[10px] text-subtle">RIV</span>82 </div>83 {riv !== null ? (84 <>85 <div className="num mt-1 text-3xl font-semibold tracking-tight text-fg">{fmtMoney(riv)}</div>86 <div className="num text-[12px] text-muted">87 Range {fmtMoney(low)} – {fmtMoney(high)}88 </div>89 </>90 ) : (91 <div className="mt-1">92 <div className="text-2xl font-semibold text-subtle">Data unavailable</div>93 <p className="text-[11px] text-muted">Insufficient verified evidence for a valuation. Guide prices and listings, if any, are shown below without being presented as market value.</p>94 </div>95 )}96 <Evidence className="mt-2" confidence={conf} sampleSize={n} updatedAt={asset.updatedAt} />97 <dl className="mt-3 grid grid-cols-2 gap-3 border-t border-border pt-3">98 <Stat label="Latest sale" value={latest !== null ? fmtMoney(latest) : '—'} sub={latestAt ? fmtRelative(latestAt) : undefined} />99 <Stat label="30D change" value={<Delta value={v ? v.change30d : asset.change30d} />} />100 <Stat label="1Y change" value={<Delta value={v ? v.change1y : asset.change1y} />} />101 <Stat label="1D change" value={<Delta value={asset.change1d} />} />102 <Stat label="All-time high" value={asset.athUsd !== null ? fmtMoney(asset.athUsd) : '—'} sub={asset.athAt ? fmtDate(asset.athAt) : undefined} />103 <Stat label="All-time low" value={asset.atlUsd !== null ? fmtMoney(asset.atlUsd) : '—'} sub={asset.atlAt ? fmtDate(asset.atlAt) : undefined} />104 <Stat label="Active listings" value={fmtNum(v ? v.activeListings : asset.activeListings)} sub={(v ? v.minAskUsd : asset.minAskUsd) !== null ? `low ask ${fmtMoney(v ? v.minAskUsd : asset.minAskUsd)}` : undefined} />105 <Stat label="Sales 30D" value={fmtNum(v ? v.sales30d : asset.sales30d)} sub={`${fmtNum(v ? v.salesCount : asset.salesCount)} all time`} />106 </dl>107 <div className="mt-3 grid grid-cols-1 gap-2 border-t border-border pt-3">108 <ScoreMeter label="Liquidity" value={v ? v.liquidityScore : asset.liquidityScore} />109 <ScoreMeter label="Rarity" value={asset.rarityScore} tone="rarity" />110 <ScoreMeter label="Data quality" value={asset.dataQuality} tone="gain" />111 </div>112 <p className="mt-2 text-[10px] leading-relaxed text-subtle">Valuation is an estimate ({confidenceLabel(conf)} confidence) from observed sales, not an offer or appraisal. RareIndex does not authenticate items.</p>113 </aside>114 </div>115 </header>116 );117}118