1import type { SaleRow, ListingRow } from '@/lib/queries/assets';2import { fmtDate, fmtMoney, fmtRelative, cn } from '@/lib/format';3import { humanize } from '@/lib/taxonomy';4import { Badge, Delta, EmptyState } from '@/components/ui/primitives';5import { GradeBadge, SourceLink, Thumb } from '@/components/market/bits';67/** Phone-first card lists for sales and listings (tables stay for md+). */8export function SalesCards({ items, className, emptyDescription = 'Sales appear once connectors ingest verified transactions.' }: { items: SaleRow[]; className?: string; emptyDescription?: string }) {9 if (!items.length) return <EmptyState title="No sales recorded" description={emptyDescription} />;10 return (11 <ul className={cn('divide-y divide-border', className)}>12 {items.map((s) => (13 <li key={s.id} className="flex items-start gap-3 px-3 py-2.5">14 <Thumb src={s.imageUrls[0]} alt="" size={44} rounded="rounded-md" />15 <div className="min-w-0 flex-1">16 <div className="flex items-baseline justify-between gap-2">17 <span className="num text-[15px] font-semibold text-fg">{fmtMoney(s.priceUsd)}</span>18 <span className="text-[11px] text-muted" title={s.saleDate.toISOString()}>19 {fmtDate(s.saleDate)}20 </span>21 </div>22 {s.price !== null && s.currency && s.currency !== 'USD' ? <div className="num text-[10px] text-subtle">{fmtMoney(s.price, s.currency)} native</div> : null}23 <p className="mt-0.5 truncate text-[12px] text-muted" title={s.rawTitle}>24 {s.rawTitle}25 </p>26 <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted">27 <GradeBadge grader={s.grader} grade={s.grade} />28 {s.condition ? <span>{humanize(s.condition)}</span> : null}29 <span>{humanize(s.saleType)}</span>30 <SourceLink name={s.sourceName} url={s.sourceUrl} />31 {s.status !== 'valid' ? <Badge tone="alert">{s.status}</Badge> : null}32 </div>33 </div>34 </li>35 ))}36 </ul>37 );38}3940export function ListingsCards({ items, className, emptyDescription = 'Live listings appear once marketplace connectors run.' }: { items: ListingRow[]; className?: string; emptyDescription?: string }) {41 if (!items.length) return <EmptyState title="No active listings" description={emptyDescription} />;42 return (43 <ul className={cn('divide-y divide-border', className)}>44 {items.map((l) => (45 <li key={l.id} className="flex items-start gap-3 px-3 py-2.5">46 <Thumb src={l.imageUrls[0]} alt="" size={44} rounded="rounded-md" />47 <div className="min-w-0 flex-1">48 <div className="flex items-baseline justify-between gap-2">49 <span className="num text-[15px] font-semibold text-fg">{fmtMoney(l.priceUsd)}</span>50 {l.discountToRiv !== null ? (51 <span className="inline-flex items-center gap-1 text-[11px]">52 <span className="text-subtle">vs RIV</span>53 <Delta value={-l.discountToRiv} />54 </span>55 ) : (56 <span className="text-[11px] text-subtle">{fmtRelative(l.lastSeenAt)}</span>57 )}58 </div>59 {l.price !== null && l.currency && l.currency !== 'USD' ? <div className="num text-[10px] text-subtle">{fmtMoney(l.price, l.currency)} native</div> : null}60 <p className="mt-0.5 truncate text-[12px] text-muted" title={l.rawTitle}>61 {l.rawTitle}62 </p>63 <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted">64 <GradeBadge grader={l.grader} grade={l.grade} />65 <span>{humanize(l.listingType)}</span>66 {l.seller ? <span className="truncate">{l.seller}</span> : null}67 {l.location ? <span className="text-subtle">{l.location}</span> : null}68 <SourceLink name={l.sourceName} url={l.sourceUrl} />69 </div>70 </div>71 </li>72 ))}73 </ul>74 );75}76