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 ? (
| Grade |
Sales |
Median |
Min |
Max |
Last sale |
{rows.map((r) => (
|
{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 ? (
| Date |
RIV |
Median |
Last sale |
Sales |
Volume |
Listings |
Low ask |
{[...snaps].reverse().slice(0, 120).map((s) => (
| {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 ? (
| Asset |
Similarity |
RIV |
Last sale |
30D |
Sales |
Liquidity |
{comps.map((c) => (
|
{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 ? (
| Source |
Type |
Trust |
Sales |
Listings |
Observations |
Last seen |
{sources.map((s) => (
|
|
{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)}` }))} />
);
}