SPB Git

spb/rareindex

Public
9.4 KB · 135 lines tsx
1import Image from 'next/image';2import Link from 'next/link';3import type { AssetDetail, VariantRow, GuidePrice } 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, guide = null }: { asset: AssetDetail; variants: VariantRow[]; activeVariant: VariantRow | null; tabHref: (v: string | null) => string; guide?: GuidePrice | null }) {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          ) : guide ? (91            <div className="mt-1">92              <div className="text-[10px] font-medium uppercase tracking-wider text-subtle">No RIV yet · latest guide price</div>93              <div className="num mt-0.5 text-3xl font-semibold tracking-tight text-fg">{fmtMoney(guide.priceUsd)}</div>94              <p className="num text-[11px] text-muted">95                {guide.sourceName} · {guide.priceKind.replace(/_/g, ' ')}96                {guide.currency !== 'USD' ? ` · ${fmtMoney(guide.price, guide.currency)}` : ''}97                {guide.variantLabel ? ` · ${guide.variantLabel}` : ''} · observed {fmtDate(guide.observationDate)}98              </p>99              <p className="mt-1 text-[11px] leading-snug text-subtle">A price-guide observation, not a transaction and not a RareIndex valuation. RIV publishes once enough verified sales exist.</p>100            </div>101          ) : asset.minAskUsd !== null ? (102            <div className="mt-1">103              <div className="text-[10px] font-medium uppercase tracking-wider text-subtle">No RIV yet · lowest current ask</div>104              <div className="num mt-0.5 text-3xl font-semibold tracking-tight text-fg">{fmtMoney(asset.minAskUsd)}</div>105              <p className="mt-1 text-[11px] leading-snug text-subtle">An asking price across {fmtNum(asset.activeListings)} live listing{asset.activeListings === 1 ? '' : 's'} — not a confirmed transaction.</p>106            </div>107          ) : (108            <div className="mt-1">109              <div className="text-2xl font-semibold text-subtle">Data unavailable</div>110              <p className="text-[11px] text-muted">Catalogued, but no verified sale, guide price or listing has been observed yet. Watch this asset to be notified when evidence arrives.</p>111            </div>112          )}113          <Evidence className="mt-2" confidence={conf} sampleSize={n} updatedAt={asset.updatedAt} />114          <dl className="mt-3 grid grid-cols-2 gap-3 border-t border-border pt-3">115            <Stat label="Latest sale" value={latest !== null ? fmtMoney(latest) : '—'} sub={latestAt ? fmtRelative(latestAt) : undefined} />116            <Stat label="30D change" value={<Delta value={v ? v.change30d : asset.change30d} />} />117            <Stat label="1Y change" value={<Delta value={v ? v.change1y : asset.change1y} />} />118            <Stat label="1D change" value={<Delta value={asset.change1d} />} />119            <Stat label="All-time high" value={asset.athUsd !== null ? fmtMoney(asset.athUsd) : '—'} sub={asset.athAt ? fmtDate(asset.athAt) : undefined} />120            <Stat label="All-time low" value={asset.atlUsd !== null ? fmtMoney(asset.atlUsd) : '—'} sub={asset.atlAt ? fmtDate(asset.atlAt) : undefined} />121            <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} />122            <Stat label="Sales 30D" value={fmtNum(v ? v.sales30d : asset.sales30d)} sub={`${fmtNum(v ? v.salesCount : asset.salesCount)} all time`} />123          </dl>124          <div className="mt-3 grid grid-cols-1 gap-2 border-t border-border pt-3">125            <ScoreMeter label="Liquidity" value={v ? v.liquidityScore : asset.liquidityScore} />126            <ScoreMeter label="Rarity" value={asset.rarityScore} tone="rarity" />127            <ScoreMeter label="Data quality" value={asset.dataQuality} tone="gain" />128          </div>129          <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>130        </aside>131      </div>132    </header>133  );134}135
Files · d93392862678cbf30e175697fcb812bc4ff0b5a7
Menu

Theme