TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import Link from 'next/link';2import type { PortfolioSummary, ValuedItem } from '@/lib/account/portfolio';3import type { Display } from '@/lib/account/display';4import { Badge, Card, CardHeader, Delta, Stat } from '@/components/ui/primitives';5import { Donut, Bars } from './charts';6import { cn, confidenceLabel, fmtPct } from '@/lib/format';78export function SummaryStats({ s, d, className }: { s: PortfolioSummary; d: Display; className?: string }) {9 return (10 <div className={cn('grid grid-cols-2 gap-4 sm:grid-cols-4', className)}>11 <Stat label="Collection value" value={s.valuedCount ? d.money(s.valueUsd) : '—'} sub={s.valuedCount ? <>{s.valuedCount}/{s.itemCount} items valued · confidence {confidenceLabel(s.confidence)}</> : `${s.itemCount} item${s.itemCount === 1 ? '' : 's'} · no valuation yet`} />12 <Stat label="Cost basis" value={s.costBasisUsd > 0 ? d.money(s.costBasisUsd) : '—'} sub={s.costBasisUsd > 0 ? 'purchase prices at acquisition-date FX' : 'add purchase prices to track returns'} />13 <Stat label="Unrealized gain" value={s.gainUsd === null ? '—' : <span className={s.gainUsd >= 0 ? 'text-gain' : 'text-loss'}>{d.money(s.gainUsd)}</span>} sub={s.gainUsd === null ? 'needs value + cost' : 'items with both value and cost'} />14 <Stat label="Return" value={<Delta value={s.returnPct} className="text-lg" />} sub={s.manualValueUsd > 0 ? `${d.money(s.manualValueUsd)} from manual values` : 'vs cost basis'} />15 </div>16 );17}1819export function AllocationCards({ s, d }: { s: PortfolioSummary; d: Display }) {20 return (21 <div className="grid gap-4 lg:grid-cols-3">22 <Card>23 <CardHeader title="Allocation by category" subtitle="Share of valued items" />24 <div className="p-4">25 <Donut data={s.allocationByCategory.map((b) => ({ label: b.label, value: b.valueUsd }))} centerValue={s.allocationByCategory.length ? String(s.allocationByCategory.length) : undefined} centerLabel="categories" />26 </div>27 </Card>28 <Card>29 <CardHeader title="Concentration & mix" subtitle="Where the risk sits" />30 <dl className="grid grid-cols-2 gap-y-2 p-4 text-xs">31 <dt className="text-subtle">Largest position</dt>32 <dd className="num text-right">{fmtPct(s.concentration.topItemShare, 0, false)}</dd>33 <dt className="text-subtle">Top 5 positions</dt>34 <dd className="num text-right">{fmtPct(s.concentration.top5Share, 0, false)}</dd>35 <dt className="text-subtle">HHI (0–1)</dt>36 <dd className="num text-right">{s.concentration.hhi === null ? '—' : s.concentration.hhi.toFixed(2)}</dd>37 <dt className="text-subtle">Graded share</dt>38 <dd className="num text-right">{fmtPct(s.gradingMix.filter((b) => b.key !== 'raw').reduce((a, b) => a + b.share, 0), 0, false)}</dd>39 <dt className="text-subtle">High-liquidity share</dt>40 <dd className="num text-right">{fmtPct(s.liquidityMix.find((b) => b.key === 'high')?.share ?? 0, 0, false)}</dd>41 <dt className="text-subtle">High-rarity share</dt>42 <dd className="num text-right">{fmtPct(s.rarityMix.find((b) => b.key === 'high')?.share ?? 0, 0, false)}</dd>43 </dl>44 </Card>45 <Card>46 <CardHeader title="Best & worst" subtitle="By return on cost" />47 <div className="p-4">48 {s.best.length ? (49 <Bars data={[...s.best.slice(0, 3).map((i) => ({ label: i.title, value: (i.gainPct ?? 0) * 100, tone: 'gain' as const })), ...s.worst.slice(0, 3).map((i) => ({ label: i.title, value: (i.gainPct ?? 0) * 100, tone: 'loss' as const }))]} format={(v) => `${v > 0 ? '+' : ''}${v.toFixed(0)}%`} />50 ) : (51 <p className="text-xs text-subtle">Add purchase prices to see performers.</p>52 )}53 <p className="mt-3 text-[11px] text-subtle">Values in {d.currency}; RIV estimates carry a confidence level.</p>54 </div>55 </Card>56 </div>57 );58}5960export function ValueSourceBadge({ item }: { item: ValuedItem }) {61 if (item.valueSource === 'variant_riv') return <Badge tone="index">RIV · {item.grader ? `${item.grader.toUpperCase()} ${item.grade ?? ''}`.trim() : 'variant'}</Badge>;62 if (item.valueSource === 'asset_riv') return <Badge tone="neutral">RIV · asset</Badge>;63 if (item.valueSource === 'manual') return <Badge tone="alert">manual</Badge>;64 return <Badge tone="neutral">no valuation</Badge>;65}6667export function AssetLink({ slug, title, className }: { slug: string; title: string; className?: string }) {68 return (69 <Link href={`/asset/${slug}`} className={cn('hover:underline', className)}>70 {title}71 </Link>72 );73}74