import Link from 'next/link'; import { imageProps } from '@/lib/images'; import { SmartImage } from '@/components/ui/smart-image'; import { glyphForFamily } from '@/lib/image-glyph'; import type { AssetDetail, VariantRow } from '@/lib/queries/assets'; import { getAssetImages, getAssetListings, getAssetObservations, getAssetSalePoints, getAssetSales, getAssetSnapshots, getAssetSources, getComparables, getGradeDistribution, getLatestValuation, getMarketplaceDistribution, getPopulation, getPriceDistribution, getSetSiblings, getSimilarAssets, attachGuidePrices, getValuationHistory } from '@/lib/queries/assets'; import { AssetCardGrid } from '@/components/market/asset-list'; import { fmtDate, fmtMoney, fmtNum, fmtRelative, cn } from '@/lib/format'; import { gradeLabel, humanize, catName } from '@/lib/taxonomy'; import { Card, CardHeader, EmptyState, Table, th, td, tdNum, Badge, Delta } from '@/components/ui/primitives'; import { LineChart } from '@/components/charts/line-chart'; import { PriceChart } from '@/components/charts/price-chart'; import { SalesCards, ListingsCards } from './sales-cards'; import { BarChart } from '@/components/charts/bar-chart'; import { SalesTable, ListingsTable } from '@/components/market/sales-table'; import { SourceLink, Thumb } from '@/components/market/bits'; import { Evidence } from '@/components/ui/evidence'; import { getAssetDepth, getAssetLiquidation } from '@/lib/queries/depth'; import { DepthLiquidationCard } from './depth-card'; import { countVerification } from '@rareindex/valuation'; import { listAssetLots } from '@/lib/queries/market-lists'; import { LotsIntelCards, LotsIntelTable } from '@/components/market/lots-table'; export const ASSET_TABS = ['overview', 'sales', 'listings', 'auctions', 'grades', 'population', 'images', 'history', 'comparables', 'analysis', 'sources'] as const; export type AssetTab = (typeof ASSET_TABS)[number]; export async function OverviewTab({ asset, variant }: { asset: AssetDetail; variant: VariantRow | null }) { const vid = variant?.id ?? ''; const [snaps, points, dist, sales, listings, obs, valuation, siblings, similar, valHist] = await Promise.all([ getAssetSnapshots(asset.id, vid), getAssetSalePoints(asset.id), getPriceDistribution(asset.id, variant?.id ?? null), getAssetSales(asset.id, { variantId: variant?.id ?? null, limit: 8 }), getAssetListings(asset.id, { variantId: variant?.id ?? null, limit: 6 }), getAssetObservations(asset.id, 12), getLatestValuation(asset.id, variant?.id ?? null), getSetSiblings(asset, 8).then(attachGuidePrices), getSimilarAssets(asset, 8).then(attachGuidePrices), getValuationHistory(asset.id, variant?.id ?? null), ]); const lots = await listAssetLots(asset.id, 6); const [depth, liquidation] = await Promise.all([ getAssetDepth(asset.id, variant?.id ?? null), getAssetLiquidation({ id: asset.id, categorySlug: asset.categorySlug, rivUsd: variant ? variant.rivUsd : asset.rivUsd, rivLowUsd: variant ? variant.rivLowUsd : asset.rivLowUsd, rivHighUsd: variant ? variant.rivHighUsd : asset.rivHighUsd }), ]); const verification = countVerification(sales.items.map((s) => s.verification)); const bandByDate = new Map(valHist.map((h) => [h.date, h])); const similarOnly = similar.filter((x) => !siblings.some((y) => y.id === x.id)); const scatter = variant ? points.filter((p) => p.variantId === variant.id) : points; const rivSeries = snaps.filter((p) => p.rivUsd != null).map((p) => ({ x: p.date, y: p.rivUsd! })); const askSeries = snaps.filter((p) => p.minAskUsd != null).map((p) => ({ x: p.date, y: p.minAskUsd! })); const obsSeries = snaps.filter((p) => p.observationUsd != null).map((p) => ({ x: p.date, y: p.observationUsd! })); const riv = variant ? variant.rivUsd : asset.rivUsd; return (

Price history

Verified sales (dots) with the daily RareIndex Valuation, lowest ask and guide observations when available. USD at historical FX.

Full history →
30 || rivSeries.length > 400 ? '1Y' : 'ALL'} reference={riv !== null ? { value: riv, label: `RIV ${fmtMoney(riv)}` } : null} sales={scatter.map((p) => ({ date: p.date, usd: p.usd, label: `${gradeLabel(p.grader, p.grade) ?? 'raw'} · ${p.sourceName}`, variantId: p.variantId }))} riv={rivSeries.map((p) => { const b = bandByDate.get(p.x); return { x: p.x, y: p.y, low: b?.lowUsd ?? null, high: b?.highUsd ?? null }; })} ask={askSeries} guide={obsSeries} emptyLabel="No priced observations yet for this asset" />
{valuation ? (
{sales.items.length ? (

Recent sales: {verification.verified} verified · {verification.likely} likely · {verification.unverified} unverified{verification.excluded ? ` · ${verification.excluded} excluded` : ''}

) : null} {Object.entries(valuation.methods).map(([k, v]) => ( ))}
{humanize(k)} {v === null ? — : fmtMoney(v)}
RIV {fmtMoney(valuation.rivUsd)}
Low – high {fmtMoney(valuation.lowUsd)} – {fmtMoney(valuation.highUsd)}
{valuation.notes.length ?
    {valuation.notes.map((n, i) =>
  • {n}
  • )}
: null} {valuation.observationsUsed ?

{valuation.observationsUsed} guide observations were used as a weak prior (weighted below transactions).

: null}
) : ( )}
{dist ? (
{[ ['Sales', fmtNum(dist.n)], ['Min', fmtMoney(dist.min)], ['P25', fmtMoney(dist.p25)], ['Median', fmtMoney(dist.median)], ['P75', fmtMoney(dist.p75)], ['Max', fmtMoney(dist.max)], ['Trimmed mean', fmtMoney(dist.trimmedMean)], ].map(([k, v]) => (
{k}
{v}
))}
) : ( )}
{obs.length ? (
    {obs.slice(0, 8).map((o) => (
  • · {humanize(o.priceKind)} {fmtMoney(o.priceUsd)} {o.observationDate}
  • ))}
) : ( )}
{lots.length ? ( All →} />
) : null}
All {sales.total} →} />
All →} />
{asset.description ? (

About

{asset.description}

) : null} {siblings.length ? (

Other assets in {asset.setName ?? 'this set'}

{asset.setSlug ? ( Whole set → ) : null}
) : null} {similarOnly.length ? (

Similar assets

Search “{asset.name}” →
) : null}
); } export async function SalesTab({ asset, variant, page }: { asset: AssetDetail; variant: VariantRow | null; page: number }) { const pageSize = 50; const res = await getAssetSales(asset.id, { variantId: variant?.id ?? null, limit: pageSize, offset: (page - 1) * pageSize, includeFlagged: true }); return (
{res.total > pageSize ? (
page {page} / {Math.ceil(res.total / pageSize)} {page > 1 ? ( ← Newer ) : null} {page * pageSize < res.total ? ( Older → ) : null}
) : null}
); } export async function ListingsTab({ asset, variant }: { asset: AssetDetail; variant: VariantRow | null }) { const [live, all] = await Promise.all([getAssetListings(asset.id, { variantId: variant?.id ?? null, limit: 100 }), getAssetListings(asset.id, { variantId: variant?.id ?? null, availability: 'all', limit: 100 })]); const ended = all.filter((l) => l.availability !== 'available'); return (
{ended.length ? (
) : null}
); } export async function AuctionsTab({ asset }: { asset: AssetDetail }) { const lots = await listAssetLots(asset.id, 100); return (
{!lots.length ? : null}
); } export async function GradesTab({ asset }: { asset: AssetDetail }) { const rows = await getGradeDistribution(asset.id); const graded = rows.filter((r) => r.grader !== 'raw'); return (
{rows.length ? ( {rows.map((r) => ( ))}
Grade Sales Median Min Max Last sale
{gradeLabel(r.grader, r.grade) ?? 'Raw'} {fmtNum(r.sales)} {fmtMoney(r.medianUsd)} {fmtMoney(r.minUsd)} {fmtMoney(r.maxUsd)} {fmtMoney(r.lastSaleUsd)} {r.lastSaleAt ? fmtRelative(r.lastSaleAt) : ''}
) : ( )}
g.medianUsd != null).map((g) => ({ label: gradeLabel(g.grader, g.grade) ?? g.grade, value: g.medianUsd!, sublabel: `n=${g.sales}` }))} />
); } export async function PopulationTab({ asset }: { asset: AssetDetail }) { const pops = await getPopulation(asset.id); const byGrader = new Map(); for (const p of pops) (byGrader.get(p.grader) ?? byGrader.set(p.grader, []).get(p.grader)!).push(p); if (!pops.length) return ; return (
{[...byGrader.entries()].map(([grader, reports]) => { const latest = reports[reports.length - 1]!; const first = reports[0]!; const grades = Object.entries(latest.byGrade).sort((a, b) => Number(b[0]) - Number(a[0])); const top = grades[0]; const gemRate = top && latest.total ? (top[1] / latest.total) : null; const growth = reports.length > 1 && first.total ? latest.total / first.total - 1 : null; return ( : null} />
Total graded
{fmtNum(latest.total)}
Highest grade
{top ? `${top[0]} (${fmtNum(top[1])})` : '—'}
Top-grade rate
{gemRate === null ? '—' : `${(gemRate * 100).toFixed(1)}%`}
Population momentum
({ label: `Grade ${g}`, value: n }))} maxBars={20} /> {reports.length > 1 ? ({ x: r.reportDate, y: r.total })) }]} /> : null}
); })}
); } export async function ImagesTab({ asset }: { asset: AssetDetail }) { const imgs = await getAssetImages(asset.id, 48); if (!imgs.length) return ; return (
{imgs.map((im) => (
{(() => { const ip = imageProps(im.url, { maxWidth: 384 }); return ; })()}
{im.role} {im.attribution ? ` · ${im.attribution}` : im.sourceId ? ` · ${im.sourceId}` : ''}
))}
); } export async function HistoryTab({ asset, variant }: { asset: AssetDetail; variant: VariantRow | null }) { const snaps = await getAssetSnapshots(asset.id, variant?.id ?? ''); return (

Daily series

RIV, median sale, lowest ask and volume from daily snapshots{variant ? ` · ${variant.label}` : ''}

s.rivUsd != null).map((s) => ({ x: s.date, y: s.rivUsd! })) }, { id: 'median', label: 'Median sale', dashed: true, points: snaps.filter((s) => s.medianUsd != null).map((s) => ({ x: s.date, y: s.medianUsd! })) }, { id: 'ask', label: 'Lowest ask', dashed: true, points: snaps.filter((s) => s.minAskUsd != null).map((s) => ({ x: s.date, y: s.minAskUsd! })) }]} emptyLabel="No snapshot history yet" />
({ x: s.date, y: s.volumeUsd ?? 0 })) }]} emptyLabel="No volume history" /> ({ x: s.date, y: s.listingsCount })) }]} emptyLabel="No listings history" />
{snaps.length ? ( {[...snaps].reverse().slice(0, 120).map((s) => ( ))}
Date RIV Median Last sale Sales Volume Listings Low ask
{s.date} {fmtMoney(s.rivUsd)} {fmtMoney(s.medianUsd)} {fmtMoney(s.latestSaleUsd)} {s.salesCount} {fmtMoney(s.volumeUsd)} {s.listingsCount} {fmtMoney(s.minAskUsd)}
) : ( )}
); } export async function ComparablesTab({ asset }: { asset: AssetDetail }) { const comps = await getComparables(asset, 12); return ( {comps.length ? ( {comps.map((c) => ( ))}
Asset Similarity RIV Last sale 30D Sales Liquidity
{c.title} {catName(c.categorySlug)} {Math.round(c.similarity * 100)}% {fmtMoney(c.rivUsd)} {fmtMoney(c.latestSaleUsd)} {fmtNum(c.salesCount)} {c.liquidityScore != null ? Math.round(c.liquidityScore) : '—'}
) : ( )}
); } export function AnalysisTab({ asset }: { asset: AssetDetail }) { return ( Open in AI Research → } /> ); } export async function SourcesTab({ asset }: { asset: AssetDetail }) { const [sources, mkt] = await Promise.all([getAssetSources(asset.id), getMarketplaceDistribution(asset.id)]); return (
{sources.length ? ( {sources.map((s) => ( ))}
Source Type Trust Sales Listings Observations Last seen
{humanize(s.sourceType)} {Math.round(s.trustScore * 100)}% {fmtNum(s.sales)} {fmtNum(s.listings)} {fmtNum(s.observations)} {s.lastSeen ? fmtRelative(s.lastSeen) : '—'}
) : ( )}
Canonical key {asset.canonicalKey} · identifiers:{' '} {Object.keys(asset.identifiers).length ? Object.entries(asset.identifiers).map(([k, v]) => {k}={v}) : 'none'} · created {fmtDate(asset.createdAt)}
({ label: m.sourceName, value: m.sales, sublabel: `volume ${fmtMoney(m.volumeUsd)}` }))} />
); }