TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { SaleRow, ListingRow } from '@/lib/queries/assets';2import { fmtDate, fmtMoney, fmtRelative, cn } from '@/lib/format';3import { humanize } from '@/lib/taxonomy';4import { EmptyState, VsRiv } from '@/components/ui/primitives';5import { FeeBasisPill, GradeBadge, SourceLink, Thumb, VerificationBadge } 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">{s.allInUsd !== null && Math.abs(s.allInUsd - s.priceUsd) >= 0.5 ? `${s.feeBasis === 'added_approximate' || s.feeBasis === 'added_default' ? '≈ ' : ''}${fmtMoney(s.allInUsd)}` : 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.allInUsd !== null && Math.abs(s.allInUsd - s.priceUsd) >= 0.5 ? (23 <div className="num flex items-center gap-1.5 text-[10px] text-subtle">24 <span>{fmtMoney(s.priceUsd)} hammer{s.price !== null && s.currency && s.currency !== 'USD' ? ` · ${fmtMoney(s.price, s.currency)}` : ''}</span>25 <FeeBasisPill basis={s.feeBasis} rate={s.buyerPremiumRate} className="text-[9px]" />26 </div>27 ) : s.price !== null && s.currency && s.currency !== 'USD' ? (28 <div className="num text-[10px] text-subtle">{fmtMoney(s.price, s.currency)} native</div>29 ) : null}30 <p className="mt-0.5 truncate text-[12px] text-muted" title={s.rawTitle}>31 {s.rawTitle}32 </p>33 <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted">34 <GradeBadge grader={s.grader} grade={s.grade} />35 {s.condition ? <span>{humanize(s.condition)}</span> : null}36 <span>{humanize(s.saleType)}</span>37 <SourceLink name={s.sourceName} url={s.sourceUrl} />38 <VerificationBadge label={s.verification} reason={s.verificationReason} />39 </div>40 </div>41 </li>42 ))}43 </ul>44 );45}4647export function ListingsCards({ items, className, emptyDescription = 'Live listings appear once marketplace connectors run.' }: { items: ListingRow[]; className?: string; emptyDescription?: string }) {48 if (!items.length) return <EmptyState title="No active listings" description={emptyDescription} />;49 return (50 <ul className={cn('divide-y divide-border', className)}>51 {items.map((l) => (52 <li key={l.id} className="flex items-start gap-3 px-3 py-2.5">53 <Thumb src={l.imageUrls[0]} alt="" size={44} rounded="rounded-md" />54 <div className="min-w-0 flex-1">55 <div className="flex items-baseline justify-between gap-2">56 <span className="num text-[15px] font-semibold text-fg">{fmtMoney(l.priceUsd)}</span>57 {l.discountToRiv !== null ? (58 <VsRiv discount={l.discountToRiv} className="text-[11px]" />59 ) : (60 <span className="text-[11px] text-subtle">{fmtRelative(l.lastSeenAt)}</span>61 )}62 </div>63 {l.price !== null && l.currency && l.currency !== 'USD' ? <div className="num text-[10px] text-subtle">{fmtMoney(l.price, l.currency)} native</div> : null}64 <p className="mt-0.5 truncate text-[12px] text-muted" title={l.rawTitle}>65 {l.rawTitle}66 </p>67 <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted">68 <GradeBadge grader={l.grader} grade={l.grade} />69 <span>{humanize(l.listingType)}</span>70 {l.seller ? <span className="truncate">{l.seller}</span> : null}71 {l.location ? <span className="text-subtle">{l.location}</span> : null}72 <SourceLink name={l.sourceName} url={l.sourceUrl} />73 </div>74 </div>75 </li>76 ))}77 </ul>78 );79}80