import Link from 'next/link'; import type { AssetDetail, VariantRow, GuidePrice } from '@/lib/queries/assets'; import { fmtMoney, fmtNum, fmtRelative, fmtDate, cn, confidenceLabel } from '@/lib/format'; import { catName, categoryCrumbs } from '@/lib/taxonomy'; import { Breadcrumbs } from '@/components/ui/page-header'; import { Badge, Delta } from '@/components/ui/primitives'; import { ScoreMeter } from '@/components/ui/evidence'; import { AskVsRivExplainer, ConfidenceExplainer, LiquidityExplainer, RarityExplainer } from './score-explainers'; import { AssetGallery, type GalleryImage } from './asset-gallery'; import { imageProps } from '@/lib/images'; import { glyphForFamily } from '@/lib/image-glyph'; import { AssetActions } from './asset-actions'; import { VariantTable } from './variant-table'; /** * Asset page header (§126): gallery + identity on the left, the valuation panel on the right. * Mobile-first: gallery → title → variant chips → valuation → metrics; desktop: two columns. */ export function AssetHeader({ asset, variants, activeVariant, tabHref, guide = null, images, watched = false, population = null }: { asset: AssetDetail; variants: VariantRow[]; activeVariant: VariantRow | null; tabHref: (v: string | null) => string; guide?: GuidePrice | null; images: GalleryImage[]; watched?: boolean; /** latest published graded population total, when a report exists */ population?: number | null }) { const v = activeVariant; const riv = v ? v.rivUsd : asset.rivUsd; const low = v ? v.rivLowUsd : asset.rivLowUsd; const high = v ? v.rivHighUsd : asset.rivHighUsd; const conf = v ? v.rivConfidence : asset.rivConfidence; const n = v ? v.rivSampleSize : asset.rivSampleSize; const latest = v ? v.latestSaleUsd : asset.latestSaleUsd; const latestAt = v ? v.latestSaleAt : asset.latestSaleAt; const confLabel = confidenceLabel(conf); const confTone = confLabel === 'High' ? 'gain' : confLabel === 'Medium' ? 'index' : confLabel === 'Low' ? 'alert' : 'neutral'; const facts: Array<[string, React.ReactNode]> = ( [ ['Set', asset.setName ? {asset.setName}{asset.setCode ? ` (${asset.setCode})` : ''} : null], ['Number', asset.number], ['Brand', asset.brand ? {asset.brand} : null], ['Franchise', asset.franchise], ['Reference', asset.reference], ['Model', asset.model], ['Release', asset.releaseDate], ['Production', asset.productionQuantity != null ? fmtNum(asset.productionQuantity) : null], ['Original MSRP', asset.originalMsrp != null ? fmtMoney(asset.originalMsrp, asset.originalMsrpCurrency ?? 'USD') : null], ] as Array<[string, React.ReactNode]> ).filter(([, val]) => val); return (
{/* identity */}
({ ...im, ...(imageProps(im.url, { maxWidth: 768 }) ?? {}), full: imageProps(im.url, { maxWidth: 1200 })?.src ?? null }))} alt={asset.title} glyph={glyphForFamily(asset.familySlug)} className="aspect-[4/3] w-full sm:aspect-square" />

{asset.title}

{catName(asset.categorySlug)} {asset.year ? {asset.year} : null} {asset.edition ? {asset.edition} : null} {asset.variant ? {asset.variant} : null} {asset.language && asset.language !== 'en' ? {asset.language.toUpperCase()} : null} {asset.rarity ? {asset.rarity} : null} {asset.verified ? Verified record : null}
{facts.length ? (
{facts.slice(0, 9).map(([k, val]) => (
{k}
{val}
))}
) : null} {variants.length ? (

Variant / grade

All variants {variants.slice(0, 16).map((x) => ( {x.label} {x.salesCount ? {x.salesCount} : null} ))}
) : null}
{/* valuation panel */}
); } function Chip({ href, active, children, title }: { href: string; active: boolean; children: React.ReactNode; title?: string }) { return ( {children} ); } function Metric({ label, value, sub }: { label: string; value: React.ReactNode | null; sub?: string }) { return (
{label}
{value ?? —}
{sub ?
{sub}
: null}
); } /** Low–high band with RIV and latest sale markers (a meter, not a chart: shows where the estimate sits). */ function RangeBar({ low, high, value, latest }: { low: number | null; high: number | null; value: number; latest: number | null }) { if (low === null || high === null || high <= low) return null; const pos = (x: number) => `${Math.max(2, Math.min(98, ((x - low) / (high - low)) * 100))}%`; return (
{latest !== null && latest >= low && latest <= high ? : null}
low {fmtMoney(low)} high {fmtMoney(high)}
); }