SPB Git

spb/rareindex

Public
8.0 KB · 118 lines tsx
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
Files · f9a634ec80792524177fc3e5fba5c7953d672423
Menu

Theme