import type { Metadata } from 'next'; import { Suspense } from 'react'; import { PageHeader } from '@/components/ui/page-header'; import { FilterBar } from '@/components/ui/filter-bar'; import { Pagination } from '@/components/ui/pagination'; import { Segmented } from '@/components/ui/tabs'; import { AssetCard, AssetList } from '@/components/market/asset-list'; import { Skeleton } from '@/components/ui/primitives'; import { attachGuidePrices, exploreAssets, getScopeCounts, categoryScope, type ExploreSort, type HasFilter } from '@/lib/queries/assets'; import { fmtNum } from '@/lib/format'; import { CATEGORIES, FAMILIES, GRADERS } from '@rareindex/taxonomy'; import { pick, sp1, spEnum, spInt, spNum, withParams, type SP } from '@/lib/search-params'; export const metadata: Metadata = { title: 'Explore collectibles', description: 'Browse tracked collectible assets across every category, filtered by grade, price, liquidity, rarity and momentum.' }; const SORTS: ExploreSort[] = ['relevance', 'riv', 'change7d', 'change30d', 'sales', 'liquidity', 'rarity', 'trending', 'newest', 'latest_sale', 'opportunity', 'name']; const HAS_OPTIONS: Array<{ id: HasFilter | ''; label: string }> = [{ id: '', label: 'All' }, { id: 'sales', label: 'With sales' }, { id: 'valuation', label: 'With valuation' }, { id: 'listings', label: 'With listings' }, { id: 'observations', label: 'With guide price' }, { id: 'images', label: 'With image' }]; const SORT_LABEL: Record = { relevance: 'Most data', name: 'Name', number: 'Set · number', riv: 'Valuation', change7d: '7D change', change30d: '30D change', sales: 'Sales', liquidity: 'Liquidity', rarity: 'Rarity', trending: 'Trending', newest: 'Newest', latest_sale: 'Last sale', opportunity: 'Value opportunity' }; const KEYS = ['category', 'grader', 'grade', 'min', 'max', 'liq', 'rar', 'mom', 'from', 'to', 'brand', 'set', 'q', 'sort', 'view', 'has']; export default async function ExplorePage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const sort = spEnum(sp, 'sort', SORTS, 'relevance'); const has = spEnum(sp, 'has', ['', 'sales', 'valuation', 'listings', 'observations', 'images'] as const, ''); const view = spEnum(sp, 'view', ['table', 'grid'] as const, 'table'); const page = spInt(sp, 'page'); const filters = { category: sp1(sp, 'category') ?? null, grader: sp1(sp, 'grader') ?? null, grade: sp1(sp, 'grade') ?? null, priceMin: spNum(sp, 'min'), priceMax: spNum(sp, 'max'), liquidityMin: spNum(sp, 'liq'), rarityMin: spNum(sp, 'rar'), momentumMin: spNum(sp, 'mom'), yearFrom: spNum(sp, 'from'), yearTo: spNum(sp, 'to'), brand: sp1(sp, 'brand') ?? null, set: sp1(sp, 'set') ?? null, q: sp1(sp, 'q') ?? null, has: has || null, sort, page, pageSize: 48, }; const categoryOptions = CATEGORIES.map((c) => ({ value: c.slug, label: `${' '.repeat(c.level)}${c.level ? '↳ ' : ''}${c.name}` })); const params = pick(sp, KEYS); return (
{HAS_OPTIONS.map((h) => ( {h.label} ))}
g.slug !== 'raw').map((g) => ({ value: g.slug, label: g.name })), width: 'w-32' }, { name: 'grade', label: 'Grade', type: 'text', placeholder: 'e.g. 10', width: 'w-20' }, { name: 'min', label: 'Min RIV $', type: 'number', placeholder: '0', width: 'w-24' }, { name: 'max', label: 'Max RIV $', type: 'number', placeholder: '∞', width: 'w-24' }, { name: 'liq', label: 'Liquidity ≥', type: 'number', placeholder: '0–100', width: 'w-20' }, { name: 'rar', label: 'Rarity ≥', type: 'number', placeholder: '0–100', width: 'w-20' }, { name: 'from', label: 'Year from', type: 'number', placeholder: '1900', width: 'w-20' }, { name: 'to', label: 'Year to', type: 'number', placeholder: '2026', width: 'w-20' }, ]} />
}>
Families: {FAMILIES.filter((f) => f.phase === 1).map((f) => ( {f.name} ))}
); } function SortMenu({ current, params }: { current: ExploreSort; params: Record }) { return (
Sort {SORT_LABEL[current]} ▾
{SORTS.map((s) => ( {SORT_LABEL[s]} ))}
); } async function Results({ filters, view, params }: { filters: Parameters[0]; view: 'table' | 'grid'; params: Record }) { const [res, counts] = await Promise.all([exploreAssets(filters), getScopeCounts({ scope: filters.category ? categoryScope(filters.category) : null, set: filters.set ?? null, brand: filters.brand ?? null })]); await attachGuidePrices(res.items); const columns = filters.sort === 'rarity' || filters.sort === 'trending' || filters.sort === 'opportunity' ? (['riv', 'change30d', 'sales', 'liquidity', 'rarity', 'trending', 'opportunity'] as const) : (['riv', 'change1d', 'change7d', 'change30d', 'latestSale', 'sales', 'listings', 'liquidity'] as const); return ( <>

{res.total.toLocaleString('en-US')} assets match · {fmtNum(counts.priced)} of {fmtNum(counts.assets)} in scope have a valuation {counts.withObservations ? ` · ${fmtNum(counts.withObservations)} carry guide prices` : ''} {counts.assets > counts.priced ? ' · valuations publish as verified sales accrue' : ''}

{view === 'grid' ? (
{res.items.map((a) => ( ))} {!res.items.length ?
: null}
) : (
)} ); }