TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import Link from 'next/link';2import type { LotRow } from '@/lib/queries/market-lists';3import { cn, confidenceLabel, fmtMoney, fmtNum, fmtRelative } from '@/lib/format';4import { Table, th, td, tdNum, EmptyState, Badge, VsRiv } from '@/components/ui/primitives';5import { FeeBasisPill, GradeBadge, Thumb } from './bits';67/**8 * Auction intelligence table (§33–§35): the house's own estimate and bid in native currency, the9 * buyer-pays all-in cost in USD (hammer + buyer's premium from the house schedule) and the gated10 * comparison with the RareIndex Valuation. Lots the worker has not assessed yet say so.11 */12export function LotsIntelTable({ items, className, showHouse = true, emptyTitle = 'No auction lots', emptyDescription = 'Lots appear once auction-house connectors publish catalogues.' }: { items: LotRow[]; className?: string; showHouse?: boolean; emptyTitle?: string; emptyDescription?: string }) {13 if (!items.length) return <EmptyState title={emptyTitle} description={emptyDescription} />;14 return (15 <Table className={className}>16 <thead>17 <tr>18 <th className={th}>Lot</th>19 {showHouse ? <th className={th}>House · auction</th> : null}20 <th className={cn(th, 'text-right')}>Estimate</th>21 <th className={cn(th, 'text-right')}>Current bid</th>22 <th className={cn(th, 'text-right')} title="Buyer-pays cost in USD: hammer + buyer's premium from the house's published or estimated schedule. VAT/duties/shipping not included.">23 All-in est.24 </th>25 <th className={cn(th, 'text-right')}>RIV</th>26 <th className={cn(th, 'text-right')} title="(all-in bid − RIV) / RIV of the lot's variant. Falls back to the low estimate (“est.”) when there is no bid. Same gates as listings.">27 Bid vs RIV28 </th>29 <th className={th}>Ends</th>30 </tr>31 </thead>32 <tbody>33 {items.map((l) => (34 <LotIntelRow key={l.id} l={l} showHouse={showHouse} />35 ))}36 </tbody>37 </Table>38 );39}4041function LotIntelRow({ l, showHouse }: { l: LotRow; showHouse: boolean }) {42 const cur = l.currency ?? 'USD';43 const hasBid = l.currentBid !== null && (l.bidCount ?? 1) > 0;44 const approx = l.feeBasis === 'added_approximate' || l.feeBasis === 'added_default';45 const allIn = l.hammerPriceUsd ?? l.allInBidUsd ?? l.allInEstimateLowUsd;46 const allInIsEstimate = l.hammerPriceUsd === null && l.allInBidUsd === null && l.allInEstimateLowUsd !== null;47 const vs = l.bidVsRiv ?? l.estimateVsRiv;48 const vsIsEstimate = l.bidVsRiv === null && l.estimateVsRiv !== null;49 return (50 <tr className="hover:bg-sunken">51 <td className={cn(td, 'max-w-[360px]')}>52 <span className="flex items-center gap-2">53 <Thumb src={l.imageUrls[0]} alt="" size={32} />54 <span className="min-w-0">55 <a href={l.url} target="_blank" rel="noopener nofollow" className="block truncate font-medium text-fg hover:underline" title={l.title}>56 {l.lotNumber ? `Lot ${l.lotNumber} · ` : ''}57 {l.title}58 </a>59 <span className="flex flex-wrap items-center gap-x-1.5 text-[11px] text-muted">60 {l.assetSlug ? (61 <Link href={`/asset/${l.assetSlug}`} className="truncate hover:text-fg">62 {l.assetTitle}63 </Link>64 ) : (65 <span className="text-subtle">Not yet matched to a canonical asset</span>66 )}67 <GradeBadge grader={l.grader} grade={l.grade} />68 </span>69 </span>70 </span>71 </td>72 {showHouse ? (73 <td className={cn(td, 'text-muted')}>74 {l.auctionHouseSlug ? (75 <Link href={`/auctions/house/${l.auctionHouseSlug}`} className="hover:text-fg">76 {l.auctionHouse}77 </Link>78 ) : (79 l.auctionHouse80 )}81 {l.auctionName ? <span className="block max-w-[200px] truncate text-[10px] text-subtle">{l.auctionName}</span> : null}82 </td>83 ) : null}84 <td className={tdNum}>85 {l.estimateLow !== null || l.estimateHigh !== null ? (86 <span className="inline-flex flex-col items-end leading-tight">87 <span>88 {fmtMoney(l.estimateLow, cur)} – {fmtMoney(l.estimateHigh, cur)}89 </span>90 {cur !== 'USD' && (l.estimateLowUsd !== null || l.estimateHighUsd !== null) ? <span className="text-[10px] text-subtle">{fmtMoney(l.estimateLowUsd)} – {fmtMoney(l.estimateHighUsd)}</span> : null}91 </span>92 ) : (93 <span className="text-subtle">—</span>94 )}95 </td>96 <td className={tdNum}>97 {l.hammerPrice !== null ? (98 <span className="inline-flex flex-col items-end leading-tight">99 <span className="font-medium">{fmtMoney(l.hammerPrice, cur)}</span>100 <span className="text-[10px] text-subtle">hammer</span>101 </span>102 ) : hasBid ? (103 <span className="inline-flex flex-col items-end leading-tight">104 <span>{fmtMoney(l.currentBid, cur)}</span>105 <span className="text-[10px] text-subtle">{l.bidCount !== null ? `${fmtNum(l.bidCount)} bid${l.bidCount === 1 ? '' : 's'}` : 'bid'}</span>106 </span>107 ) : l.currentBid !== null ? (108 <span className="inline-flex flex-col items-end leading-tight text-muted" title="Opening price — no bid placed yet">109 <span>{fmtMoney(l.currentBid, cur)}</span>110 <span className="text-[10px] text-subtle">opening · 0 bids</span>111 </span>112 ) : (113 <span className="text-subtle">—</span>114 )}115 </td>116 <td className={tdNum}>117 {allIn !== null ? (118 <span className="inline-flex flex-col items-end leading-tight">119 <span className="font-medium text-fg">120 {approx ? '≈ ' : ''}121 {fmtMoney(allIn)}122 </span>123 <span className="flex items-center gap-1 text-[10px] text-subtle">124 {allInIsEstimate ? <span>on low est.</span> : null}125 <FeeBasisPill basis={l.feeBasis} rate={l.buyerPremiumRate} className="text-[9px]" />126 </span>127 </span>128 ) : l.assessedAt ? (129 <span className="text-subtle" title="No price to assess (no bid and no estimate)">—</span>130 ) : (131 <span className="text-[10px] text-subtle">Not assessed yet</span>132 )}133 </td>134 <td className={tdNum}>135 {l.rivUsd !== null ? (136 <span className="inline-flex flex-col items-end leading-tight">137 <span>{fmtMoney(l.rivUsd)}</span>138 <span className="text-[10px] text-subtle">139 {confidenceLabel(l.rivConfidence)} · n={l.rivSampleSize}140 </span>141 </span>142 ) : (143 <span className="text-subtle">—</span>144 )}145 </td>146 <td className={tdNum}>147 {vs !== null ? (148 <span className="inline-flex flex-col items-end leading-tight">149 <VsRiv discount={vs} showLabel={false} />150 {vsIsEstimate ? <span className="text-[10px] text-subtle">est. vs RIV</span> : null}151 </span>152 ) : l.assessedAt && l.assessmentVerdict === 'ungated' ? (153 <span className="text-subtle" title="Not compared: the valuation does not meet the gates (transaction-based RIV, ≥ 5 sales, medium+ confidence, same variant)">—</span>154 ) : l.assessedAt && l.assessmentVerdict === 'anomaly' ? (155 <Badge tone="alert">Data/identity anomaly</Badge>156 ) : l.assessedAt ? (157 <span className="text-subtle">—</span>158 ) : (159 <span className="text-[10px] text-subtle">Not assessed yet</span>160 )}161 </td>162 <td className={cn(td, 'whitespace-nowrap text-muted')}>163 {l.endsAt ? <span title={l.endsAt.toISOString()}>{fmtRelative(l.endsAt)}</span> : '—'}164 <Badge tone={l.status === 'live' ? 'gain' : l.status === 'upcoming' ? 'index' : 'neutral'} className="ml-1.5">165 {l.status}166 </Badge>167 </td>168 </tr>169 );170}171172/** Card list of lots for phones (image, title, house, all-in, bid vs RIV, ends). */173export function LotsIntelCards({ items, className }: { items: LotRow[]; className?: string }) {174 if (!items.length) return null;175 return (176 <ul className={cn('divide-y divide-border', className)}>177 {items.map((l) => {178 const allIn = l.hammerPriceUsd ?? l.allInBidUsd ?? l.allInEstimateLowUsd;179 const vs = l.bidVsRiv ?? l.estimateVsRiv;180 const approx = l.feeBasis === 'added_approximate' || l.feeBasis === 'added_default';181 return (182 <li key={l.id}>183 <a href={l.url} target="_blank" rel="noopener nofollow" className="flex min-h-[64px] items-center gap-3 px-3 py-2.5 hover:bg-sunken">184 <Thumb src={l.imageUrls[0]} alt="" size={44} rounded="rounded-[5px]" />185 <span className="min-w-0 flex-1">186 <span className="line-clamp-2 text-[13px] font-medium leading-[1.3] text-fg">{l.title}</span>187 <span className="mt-0.5 block truncate text-[11px] text-muted">188 {l.auctionHouse}189 {l.endsAt ? ` · ends ${fmtRelative(l.endsAt)}` : ''}190 </span>191 </span>192 <span className="num flex shrink-0 flex-col items-end leading-tight">193 <span className="text-[14px] font-semibold text-fg">{allIn !== null ? `${approx ? '≈ ' : ''}${fmtMoney(allIn)}` : l.currentBid !== null ? fmtMoney(l.currentBid, l.currency ?? 'USD') : l.estimateHigh !== null ? `est. ${fmtMoney(l.estimateHigh, l.currency ?? 'USD')}` : '—'}</span>194 {vs !== null ? <VsRiv discount={vs} className="text-[11px]" showLabel={false} /> : <span className="text-[10px] text-subtle">{allIn !== null ? 'all-in' : 'not assessed'}</span>}195 </span>196 </a>197 </li>198 );199 })}200 </ul>201 );202}203