TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { requireUser } from '@/lib/auth/session';4import { listCollections, portfolioHistory, indexSeries } from '@/lib/account/queries';5import { summarizePortfolio, rebase } from '@/lib/account/portfolio';6import { loadItems } from '@/lib/account/queries';7import { getDisplay } from '@/lib/account/display';8import { PageHeader, btnPrimary, btnSecondary } from '@/components/account/page-header';9import { SummaryStats, AllocationCards } from '@/components/account/portfolio-widgets';10import { LineChart } from '@/components/account/charts';11import { Card, CardHeader, Delta, EmptyState, Badge } from '@/components/ui/primitives';12import { fmtRelative } from '@/lib/format';13import { CreateCollectionForm } from './create-form';1415export const metadata: Metadata = { title: 'Collections', robots: { index: false } };1617export default async function CollectionsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {18 const sp = await searchParams;19 const u = await requireUser('/collections');20 const d = await getDisplay();21 const cols = await listCollections(u.id);22 const allItems = await loadItems(cols.map((c) => c.collection.id));23 const total = summarizePortfolio(allItems);24 const history = await portfolioHistory(u.id);25 const rare = history.length ? await indexSeries('RARE', history[0]!.date) : [];26 const mine = rebase(history.map((h) => ({ date: h.date, value: h.valueUsd })));27 const bench = rebase(rare);2829 return (30 <>31 {sp.welcome ? (32 <div className="mb-5 rounded-md border border-index/30 bg-index-bg px-4 py-3 text-sm">33 <p className="font-medium">Welcome to RareIndex{u.name ? `, ${u.name}` : ''}.</p>34 <p className="mt-0.5 text-muted">Start by creating a collection and adding what you own. Every item is valued with the RareIndex Valuation (RIV) when enough market evidence exists — and marked honestly when it does not.</p>35 </div>36 ) : null}37 <PageHeader38 title="Collections"39 description="Your portfolio across every collection, valued with RIV and your cost basis at acquisition-date exchange rates."40 actions={41 <>42 <Link href="/deals" className={btnSecondary}>43 Deal Radar44 </Link>45 <a href="#new" className={btnPrimary}>46 New collection47 </a>48 </>49 }50 />51 {d.fallback ? <p className="mb-3 text-xs text-alert">No exchange rate loaded for your display currency yet — values are shown in USD.</p> : null}52 <SummaryStats s={total} d={d} className="mb-5" />5354 <div className="mb-5 grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">55 <Card>56 <CardHeader title="Portfolio value" subtitle={history.length ? `${history.length} daily snapshots · vs RARE index (rebased to 1000)` : 'Daily snapshots begin after your first valued item'} action={<Link href="/my-index" className="text-muted hover:text-fg">My Index →</Link>} />57 <div className="p-4">58 <LineChart series={[{ name: 'My portfolio', points: mine }, ...(bench.length > 1 ? [{ name: 'RARE', points: bench, color: 'var(--ri-fg-subtle)', dashed: true }] : [])]} height={200} />59 {history.length ? (60 <div className="mt-2 flex flex-wrap gap-4 text-xs text-muted">61 <span>62 <span className="mr-1 inline-block h-2 w-3 rounded-sm bg-index align-middle" /> My portfolio63 </span>64 {bench.length > 1 ? (65 <span>66 <span className="mr-1 inline-block h-0.5 w-3 border-t border-dashed border-subtle align-middle" /> RARE Global Collectibles Index67 </span>68 ) : (69 <span className="text-subtle">RARE index history is not available yet for this period.</span>70 )}71 </div>72 ) : null}73 </div>74 </Card>75 <Card>76 <CardHeader title="At a glance" />77 <dl className="grid grid-cols-2 gap-y-2 p-4 text-xs">78 <dt className="text-subtle">Collections</dt>79 <dd className="num text-right">{cols.length}</dd>80 <dt className="text-subtle">Items · units</dt>81 <dd className="num text-right">82 {total.itemCount} · {total.unitCount}83 </dd>84 <dt className="text-subtle">Categories</dt>85 <dd className="num text-right">{total.allocationByCategory.length}</dd>86 <dt className="text-subtle">Awaiting valuation</dt>87 <dd className="num text-right">{total.unvaluedCount}</dd>88 <dt className="text-subtle">Display currency</dt>89 <dd className="text-right">{d.currency}</dd>90 </dl>91 <div className="border-t border-border p-4 text-xs text-muted">92 RIV = RareIndex Valuation. Each item shows whether its value comes from a graded-variant RIV, the asset-level RIV, or a manual value you entered. Nothing is invented: items without market evidence stay unvalued.93 </div>94 </Card>95 </div>9697 {total.valuedCount > 0 ? <div className="mb-5"><AllocationCards s={total} d={d} /></div> : null}9899 <section className="mb-6">100 <h2 className="mb-3 text-base font-semibold tracking-tight">Your collections</h2>101 {cols.length === 0 ? (102 <Card>103 <EmptyState title="No collections yet" description="Create your first collection below. You can keep separate collections per category, per vault, or a wishlist with a budget." />104 </Card>105 ) : (106 <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">107 {cols.map(({ collection: c, summary: s }) => (108 <Link key={c.id} href={`/collections/${c.id}`} className="card block p-4 transition hover:border-border-strong">109 <div className="flex items-start justify-between gap-2">110 <div className="min-w-0">111 <p className="flex items-center gap-2 truncate text-sm font-semibold">112 {c.color ? <span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ background: c.color }} /> : null}113 {c.name}114 </p>115 <p className="mt-0.5 text-xs text-muted">116 {s.itemCount} item{s.itemCount === 1 ? '' : 's'} · updated {fmtRelative(c.updatedAt)}117 </p>118 </div>119 <div className="flex gap-1">120 {c.kind !== 'collection' ? <Badge tone="neutral">{c.kind}</Badge> : null}121 {c.isPublic ? <Badge tone="index">public</Badge> : null}122 </div>123 </div>124 <div className="mt-3 flex items-end justify-between">125 <div>126 <p className="num text-lg font-semibold">{s.valuedCount ? d.money(s.valueUsd) : '—'}</p>127 <p className="text-[11px] text-subtle">{s.valuedCount ? `${s.valuedCount}/${s.itemCount} valued` : 'no valuation yet'}</p>128 </div>129 <Delta value={s.returnPct} />130 </div>131 {c.kind === 'wishlist' && c.budgetUsd ? (132 <p className="mt-2 text-[11px] text-muted">133 Budget {d.money(c.budgetUsd)} · wishlist value {d.money(s.valueUsd)}134 </p>135 ) : null}136 </Link>137 ))}138 </div>139 )}140 </section>141142 <div id="new"><Card>143 <CardHeader title="New collection" subtitle="Private by default. You can share it publicly later from its page." />144 <div className="p-4">145 <CreateCollectionForm />146 </div>147 </Card></div>148 </>149 );150}151