import Link from 'next/link'; import type { ReactNode } from 'react'; import type { AssetCard as AssetCardData } from '@/lib/queries/assets'; import { cn, confidenceLabel, fmtMoney, fmtNum, fmtRelative } from '@/lib/format'; import { catName } from '@/lib/taxonomy'; import { Delta, VsRiv } from '@/components/ui/primitives'; export type TileMetric = 'change7d' | 'change1d' | 'change30d' | 'trending' | 'watchers' | 'riv' | 'opportunity' | 'new' | 'guide' | 'latestSale'; import { TileImage } from '@/components/ui/tile-image'; export { TileImage }; /** Price line: RIV with confidence, or the guide price honestly labelled, or the latest sale. */ export function PriceLine({ a, size = 'md', className }: { a: AssetCardData; size?: 'sm' | 'md' | 'lg'; className?: string }) { const valueCls = size === 'lg' ? 'text-xl' : size === 'sm' ? 'text-[13px]' : 'text-[15px]'; if (a.rivUsd !== null) { return ( {fmtMoney(a.rivUsd)} RIV · {confidenceLabel(a.rivConfidence)} · n={a.rivSampleSize} ); } if (a.guideUsd !== null && a.guideUsd !== undefined) { return ( {fmtMoney(a.guideUsd)} Guide · {a.guideSource ?? a.guideKind ?? 'observed'} ); } if (a.latestSaleUsd !== null) { return ( {fmtMoney(a.latestSaleUsd)} Last sale · {fmtRelative(a.latestSaleAt)} ); } return ( — Catalogued · no price yet ); } function MetricNode({ a, metric }: { a: AssetCardData; metric: TileMetric }) { switch (metric) { case 'change1d': return ; case 'change7d': return ; case 'change30d': return ; case 'trending': return score {a.trendingScore?.toFixed(1) ?? '—'}; case 'watchers': return {fmtNum(a.watchers)} watching; case 'opportunity': return ( ); case 'new': return added {fmtRelative(a.updatedAt)}; case 'riv': return {fmtNum(a.salesCount)} sales; case 'latestSale': return a.latestSaleUsd !== null ? last {fmtMoney(a.latestSaleUsd)} : null; default: return null; } } /** Poster-style asset tile for rails and grids. */ export function AssetTile({ a, metric = 'change30d', className, priority = false, width = 'w-[164px] sm:w-[188px]' }: { a: AssetCardData; metric?: TileMetric; className?: string; priority?: boolean; width?: string }) { return ( {a.title} {a.setName ?? catName(a.categorySlug)} {a.year ? ` · ${a.year}` : ''} ); } /** Horizontal snap rail on small screens; wraps into a grid from lg. */ export function Rail({ children, className, cols = 'lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8' }: { children: ReactNode; className?: string; cols?: string }) { return
*]:w-auto', cols, className)}>{children}
; } export function RailHeader({ title, subtitle, href, hrefLabel = 'View all', className }: { title: ReactNode; subtitle?: ReactNode; href?: string; hrefLabel?: string; className?: string }) { return (

{title}

{subtitle ?

{subtitle}

: null}
{href ? ( {hrefLabel} → ) : null}
); } export function TileSkeleton({ width = 'w-[164px] sm:w-[188px]' }: { width?: string }) { return (
); }