SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
5.5 KB · 106 lines tsx
Raw Blame History
1import Link from 'next/link';2import { ExternalLink } from 'lucide-react';3import { cn, fmtMoney, fmtPct } from '@/lib/format';4import { feeBasisLabel } from '@rareindex/valuation';5import { catName, gradeLabel } from '@/lib/taxonomy';6import { Badge } from '@/components/ui/primitives';78/** Small shared building blocks for market tables and cards. */910import { Thumb } from '@/components/ui/tile-image';11export { Thumb };1213export function CategoryTag({ slug, className }: { slug: string | null | undefined; className?: string }) {14  if (!slug) return null;15  return (16    <Link href={`/markets/${slug}`} className={cn('text-[11px] text-muted hover:text-fg', className)}>17      {catName(slug)}18    </Link>19  );20}2122export function GradeBadge({ grader, grade, className }: { grader: string | null | undefined; grade: string | null | undefined; className?: string }) {23  const label = gradeLabel(grader, grade);24  if (!label) return <span className={cn('text-subtle', className)}>—</span>;25  return (26    <Badge tone={grader && grader !== 'raw' ? 'index' : 'neutral'} className={className}>27      {label}28    </Badge>29  );30}3132/** §178: source attribution with outbound link. */33export function SourceLink({ name, url, className }: { name: string; url: string | null | undefined; className?: string }) {34  if (!url) return <span className={cn('text-muted', className)}>{name}</span>;35  return (36    <a href={url} target="_blank" rel="noopener nofollow" className={cn('inline-flex items-center gap-1 text-muted hover:text-fg', className)} title={`View on ${name}`}>37      {name}38      <ExternalLink className="h-3 w-3" aria-hidden />39    </a>40  );41}4243export function PriceCell({ usd, native, currency, className }: { usd: number | null | undefined; native?: number | null; currency?: string | null; className?: string }) {44  const showNative = native !== null && native !== undefined && currency && currency !== 'USD';45  return (46    <span className={cn('num inline-flex flex-col items-end leading-tight', className)}>47      <span className="font-medium text-fg">{fmtMoney(usd)}</span>48      {showNative ? <span className="text-[10px] text-subtle">{fmtMoney(native, currency!)}</span> : null}49    </span>50  );51}5253export function AssetTitleCell({ slug, title, categorySlug, image, sub, size = 36 }: { slug: string; title: string; categorySlug?: string | null; image?: string | null; sub?: string | null; size?: number }) {54  return (55    <span className="flex min-w-0 items-center gap-2.5">56      <Thumb src={image} alt="" size={size} />57      <span className="min-w-0">58        <Link href={`/asset/${slug}`} className="block truncate font-medium text-fg hover:underline" title={title}>59          {title}60        </Link>61        <span className="block truncate text-[11px] text-muted">62          {categorySlug ? catName(categorySlug) : null}63          {sub ? (categorySlug ? ` · ${sub}` : sub) : null}64        </span>65      </span>66    </span>67  );68}6970/** Heuristic sale verification label (§39, §205): verified · likely · unverified · excluded. Never a claim of independent confirmation. */71export function VerificationBadge({ label, reason, className }: { label: 'verified' | 'likely' | 'unverified' | 'excluded'; reason?: string; className?: string }) {72  const tone = label === 'verified' ? 'gain' : label === 'likely' ? 'neutral' : 'alert';73  const text = label === 'verified' ? 'Verified' : label === 'likely' ? 'Likely' : label === 'unverified' ? 'Unverified' : 'Excluded';74  return (75    <Badge tone={tone} className={className}>76      <span title={`${text} sale — heuristic on source type, sale type, match confidence and source trust${reason ? `: ${reason}` : ''}`}>{text}</span>77    </Badge>78  );79}8081/** Fee basis of an all-in figure (§35): premium included / added (published, ≈ estimated, default) / none / unknown. */82export function FeeBasisPill({ basis, rate, className }: { basis: string | null | undefined; rate?: number | null; className?: string }) {83  if (!basis) return null;84  const approx = basis === 'added_approximate' || basis === 'added_default';85  const text =86    basis === 'included' ? 'premium included' : basis === 'none' ? 'no buyer premium' : basis === 'unknown' ? 'fees unknown' : `+ premium ${approx ? '≈ ' : ''}${rate !== null && rate !== undefined ? fmtPct(rate, rate < 0.1 ? 1 : 0, false) : ''}`.trim();87  const tone = basis === 'unknown' ? 'alert' : basis === 'included' || basis === 'none' ? 'neutral' : 'index';88  return (89    <Badge tone={tone} className={className}>90      <span title={`${feeBasisLabel(basis)}. VAT/sales tax on the premium, duties and shipping are not included.`}>{text}</span>91    </Badge>92  );93}9495/** Buyer-pays amount in USD with the fee basis under it; falls back to the recorded price when no all-in figure exists. */96export function AllInCell({ allInUsd, priceUsd, basis, rate, className }: { allInUsd: number | null | undefined; priceUsd: number | null | undefined; basis?: string | null; rate?: number | null; className?: string }) {97  const v = allInUsd ?? priceUsd;98  const differs = allInUsd !== null && allInUsd !== undefined && priceUsd !== null && priceUsd !== undefined && Math.abs(allInUsd - priceUsd) >= 0.5;99  return (100    <span className={cn('num inline-flex flex-col items-end leading-tight', className)}>101      <span className="font-medium text-fg">{basis === 'added_approximate' || basis === 'added_default' ? '≈ ' : ''}{fmtMoney(v)}</span>102      {differs || (basis && basis !== 'none') ? <FeeBasisPill basis={basis} rate={rate} className="mt-0.5 text-[9px]" /> : null}103    </span>104  );105}106