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%
4.0 KB · 58 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import { Suspense } from 'react';3import { PageHeader } from '@/components/ui/page-header';4import { FilterBar } from '@/components/ui/filter-bar';5import { Pagination } from '@/components/ui/pagination';6import { Skeleton } from '@/components/ui/primitives';7import { ListingsTable } from '@/components/market/sales-table';8import { listListings, listListingSources } from '@/lib/queries/market-lists';9import { CATEGORIES, GRADERS } from '@rareindex/taxonomy';10import { pick, sp1, spEnum, spInt, spNum, type SP } from '@/lib/search-params';1112export const metadata: Metadata = { title: 'Listings', description: 'Live asking prices across marketplaces, compared with the RareIndex Valuation to surface value opportunities.' };1314const KEYS = ['category', 'source', 'grader', 'min', 'max', 'disc', 'type', 'q', 'sort'];1516export default async function ListingsPage({ searchParams }: { searchParams: Promise<SP> }) {17  const sp = await searchParams;18  const sources = await listListingSources();19  const filters = { category: sp1(sp, 'category') ?? null, source: sp1(sp, 'source') ?? null, grader: sp1(sp, 'grader') ?? null, minUsd: spNum(sp, 'min'), maxUsd: spNum(sp, 'max'), minDiscount: spNum(sp, 'disc') === null ? null : spNum(sp, 'disc')! / 100, listingType: sp1(sp, 'type') ?? null, q: sp1(sp, 'q') ?? null, sort: spEnum(sp, 'sort', ['newest', 'price_asc', 'price_desc', 'discount', 'ending'] as const, 'newest'), page: spInt(sp, 'page'), pageSize: 50 };20  return (21    <div>22      <PageHeader kicker="Marketplaces" title="Listings" description="Current asks observed on public marketplaces. A listing price is not a market value; the “vs RIV” column shows the discount or premium to the RareIndex Valuation with its confidence — analytical data, not investment advice (§123)." compact />23      <Suspense>24        <FilterBar25          className="mb-3"26          fields={[27            { name: 'q', label: 'Search', type: 'text', placeholder: 'asset or title…', width: 'w-44' },28            { name: 'category', label: 'Category', type: 'select', options: CATEGORIES.map((c) => ({ value: c.slug, label: `${c.level ? '↳ ' : ''}${c.name}` })), width: 'w-48' },29            { name: 'source', label: 'Marketplace', type: 'select', options: sources.map((s) => ({ value: s.id, label: `${s.name} (${s.listings.toLocaleString('en-US')})` })), width: 'w-44' },30            { name: 'grader', label: 'Grader', type: 'select', options: GRADERS.map((g) => ({ value: g.slug, label: g.name })), width: 'w-28' },31            { name: 'type', label: 'Type', type: 'select', options: ['fixed_price', 'auction', 'best_offer', 'ask'].map((t) => ({ value: t, label: t.replace('_', ' ') })), width: 'w-28' },32            { name: 'min', label: 'Min $', type: 'number', width: 'w-24' },33            { name: 'max', label: 'Max $', type: 'number', width: 'w-24' },34            { name: 'disc', label: 'Discount ≥ %', type: 'number', placeholder: 'e.g. 15', width: 'w-24' },35            { name: 'sort', label: 'Sort', type: 'select', options: [{ value: 'newest', label: 'Newest' }, { value: 'discount', label: 'Biggest discount' }, { value: 'price_asc', label: 'Price ↑' }, { value: 'price_desc', label: 'Price ↓' }, { value: 'ending', label: 'Ending soon' }], placeholder: 'Newest', width: 'w-36' },36          ]}37        />38      </Suspense>39      <Suspense fallback={<Skeleton className="h-96" />}>40        <Results filters={filters} params={pick(sp, KEYS)} />41      </Suspense>42    </div>43  );44}4546async function Results({ filters, params }: { filters: Parameters<typeof listListings>[0]; params: Record<string, string | undefined> }) {47  const res = await listListings(filters);48  return (49    <>50      <p className="mb-2 num text-[11px] text-subtle">{res.total.toLocaleString('en-US')} active listings</p>51      <div className="card overflow-hidden">52        <ListingsTable items={res.items} />53      </div>54      <Pagination page={res.page} pageSize={res.pageSize} total={res.total} basePath="/listings" params={params} />55    </>56  );57}58