import Link from 'next/link'; import type { AssetCard as AssetCardData } from '@/lib/queries/assets'; import { fmtMoney, fmtNum, fmtRelative, confidenceLabel, cn } from '@/lib/format'; import { catName } from '@/lib/taxonomy'; import { Delta, Table, th, td, tdNum, EmptyState, VsRiv } from '@/components/ui/primitives'; import { Thumb } from './bits'; import { PriceLine, TileImage } from './asset-tile'; export type AssetColumn = 'riv' | 'change1d' | 'change7d' | 'change30d' | 'change1y' | 'latestSale' | 'sales' | 'sales30d' | 'listings' | 'minAsk' | 'liquidity' | 'rarity' | 'trending' | 'opportunity' | 'watchers' | 'updated'; const DEFAULT_COLUMNS: AssetColumn[] = ['riv', 'change7d', 'change30d', 'latestSale', 'sales', 'listings', 'liquidity']; const HEAD: Record = { riv: 'RIV (USD)', change1d: '1D', change7d: '7D', change30d: '30D', change1y: '1Y', latestSale: 'Last sale', sales: 'Sales', sales30d: 'Sales 30D', listings: 'Listings', minAsk: 'Low ask', liquidity: 'Liquidity', rarity: 'Rarity', trending: 'Trending', opportunity: 'Value opp.', watchers: 'Watchers', updated: 'Updated', }; function cell(a: AssetCardData, c: AssetColumn) { switch (c) { case 'riv': return a.rivUsd === null ? ( a.guideUsd != null ? ( {fmtMoney(a.guideUsd)} guide · {a.guideSource ?? a.guideKind ?? 'observed'} ) : ( — ) ) : ( {fmtMoney(a.rivUsd)} {confidenceLabel(a.rivConfidence)} · n={a.rivSampleSize} ); case 'change1d': return ; case 'change7d': return ; case 'change30d': return ; case 'change1y': return ; case 'latestSale': return a.latestSaleUsd === null ? ( — ) : ( {fmtMoney(a.latestSaleUsd)} {fmtRelative(a.latestSaleAt)} ); case 'sales': return fmtNum(a.salesCount); case 'sales30d': return fmtNum(a.sales30d); case 'listings': return fmtNum(a.activeListings); case 'minAsk': return fmtMoney(a.minAskUsd); case 'liquidity': return a.liquidityScore === null ? — : Math.round(a.liquidityScore); case 'rarity': return a.rarityScore === null ? — : Math.round(a.rarityScore); case 'trending': return a.trendingScore === null ? — : a.trendingScore.toFixed(1); case 'opportunity': return ; case 'watchers': return fmtNum(a.watchers); case 'updated': return {fmtRelative(a.updatedAt)}; } } export function AssetTable({ items, columns = DEFAULT_COLUMNS, rank = false, startRank = 1, className, emptyTitle = 'No assets yet', emptyDescription = 'Assets appear here as connectors ingest catalogues, sales and listings.' }: { items: AssetCardData[]; columns?: AssetColumn[]; rank?: boolean; startRank?: number; className?: string; emptyTitle?: string; emptyDescription?: string }) { if (!items.length) return ; return ( {rank ? : null} {columns.map((c) => ( ))} {items.map((a, i) => ( {rank ? : null} {columns.map((c) => ( ))} ))}
#Asset {HEAD[c]}
{startRank + i} {a.title} {catName(a.categorySlug)} {a.setName ? ` · ${a.setName}` : ''} {a.year ? ` · ${a.year}` : ''} {cell(a, c)}
); } /** Compact horizontal card for mobile lists and grids: image, title, set/year, honest price line, one metric. */ export function AssetCard({ a, className, metric = 'change30d' }: { a: AssetCardData; className?: string; metric?: 'change30d' | 'change7d' | 'change1d' | 'latestSale' | 'opportunity' | 'trending' }) { const value = metric === 'change1d' ? a.change1d : metric === 'change7d' ? a.change7d : metric === 'change30d' ? a.change30d : null; const metricNode = metric === 'latestSale' ? ( a.latestSaleUsd !== null && a.rivUsd !== null ? last {fmtMoney(a.latestSaleUsd)} : a.number ? #{a.number} : null ) : metric === 'opportunity' ? ( a.valueOpportunity !== null ? : null ) : metric === 'trending' ? ( a.trendingScore !== null ? trend {a.trendingScore.toFixed(1)} : null ) : value !== null ? ( {metric === 'change1d' ? '1D' : metric === 'change7d' ? '7D' : '30D'} ) : a.salesCount ? ( {fmtNum(a.salesCount)} sales ) : null; return ( {a.title} {catName(a.categorySlug)} {a.setName ? ` · ${a.setName}` : ''} {a.year ? ` · ${a.year}` : ''} {metricNode} ); } export function AssetCardGrid({ items, metric, className }: { items: AssetCardData[]; metric?: Parameters[0]['metric']; className?: string }) { if (!items.length) return null; return (
{items.map((a) => ( ))}
); } /** Responsive list: cards on small screens, table from md up. */ export function AssetList(props: Parameters[0] & { metric?: Parameters[0]['metric']; forceCards?: boolean }) { if (!props.items.length) return ; if (props.forceCards) return ; return ( <>
); }