SPB Git

spb/rareindex

Public
3.4 KB · 74 lines tsx
1import Image from 'next/image';2import Link from 'next/link';3import { ExternalLink } from 'lucide-react';4import { cn, fmtMoney } from '@/lib/format';5import { catName, gradeLabel } from '@/lib/taxonomy';6import { Badge } from '@/components/ui/primitives';78/** Small shared building blocks for market tables and cards. */910export function Thumb({ src, alt, size = 40, className, rounded = 'rounded-sm' }: { src: string | null | undefined; alt: string; size?: number; className?: string; rounded?: string }) {11  return (12    <span className={cn('relative inline-block shrink-0 overflow-hidden bg-inset', rounded, className)} style={{ width: size, height: size }}>13      {src ? <Image src={src} alt={alt} fill sizes={`${size}px`} className="object-cover" unoptimized={src.endsWith('.svg')} /> : <span className="absolute inset-0 flex items-center justify-center text-[9px] uppercase tracking-wider text-subtle">n/a</span>}14    </span>15  );16}1718export function CategoryTag({ slug, className }: { slug: string | null | undefined; className?: string }) {19  if (!slug) return null;20  return (21    <Link href={`/markets/${slug}`} className={cn('text-[11px] text-muted hover:text-fg', className)}>22      {catName(slug)}23    </Link>24  );25}2627export function GradeBadge({ grader, grade, className }: { grader: string | null | undefined; grade: string | null | undefined; className?: string }) {28  const label = gradeLabel(grader, grade);29  if (!label) return <span className={cn('text-subtle', className)}>—</span>;30  return (31    <Badge tone={grader && grader !== 'raw' ? 'index' : 'neutral'} className={className}>32      {label}33    </Badge>34  );35}3637/** §178: source attribution with outbound link. */38export function SourceLink({ name, url, className }: { name: string; url: string | null | undefined; className?: string }) {39  if (!url) return <span className={cn('text-muted', className)}>{name}</span>;40  return (41    <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}`}>42      {name}43      <ExternalLink className="h-3 w-3" aria-hidden />44    </a>45  );46}4748export function PriceCell({ usd, native, currency, className }: { usd: number | null | undefined; native?: number | null; currency?: string | null; className?: string }) {49  const showNative = native !== null && native !== undefined && currency && currency !== 'USD';50  return (51    <span className={cn('num inline-flex flex-col items-end leading-tight', className)}>52      <span className="font-medium text-fg">{fmtMoney(usd)}</span>53      {showNative ? <span className="text-[10px] text-subtle">{fmtMoney(native, currency!)}</span> : null}54    </span>55  );56}5758export function AssetTitleCell({ slug, title, categorySlug, image, sub, size = 36 }: { slug: string; title: string; categorySlug?: string | null; image?: string | null; sub?: string | null; size?: number }) {59  return (60    <span className="flex min-w-0 items-center gap-2.5">61      <Thumb src={image} alt="" size={size} />62      <span className="min-w-0">63        <Link href={`/asset/${slug}`} className="block truncate font-medium text-fg hover:underline" title={title}>64          {title}65        </Link>66        <span className="block truncate text-[11px] text-muted">67          {categorySlug ? catName(categorySlug) : null}68          {sub ? (categorySlug ? ` · ${sub}` : sub) : null}69        </span>70      </span>71    </span>72  );73}74
Files · d93392862678cbf30e175697fcb812bc4ff0b5a7
Menu

Theme