spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import Link from "next/link";2import { InstrumentTable } from "@/components/market/instrument-table";3import { Page, PageHeader } from "@/components/ui/section";4import { apiEnvelope } from "@/lib/api";5import { cx } from "@/lib/format";6import type { InstrumentWithQuote } from "@/lib/types";78const SORTS: Array<[string, string]> = [["-change", "Top gainers"], ["change", "Top losers"], ["volume", "Most active"], ["symbol", "A → Z"]];910/**11 * Shared server page for one asset-class list (/stocks, /crypto, …). Fetches quoted instruments12 * from /v1/instruments and renders a live table; pagination and sort via search params.13 */14export async function ClassPage({ classes, title, kicker, lead, basePath, searchParams, showExchange = true, defaultSort = "-change", note }: { classes: string[]; title: string; kicker?: string; lead: string; basePath: string; searchParams: Promise<Record<string, string | string[] | undefined>>; showExchange?: boolean; defaultSort?: string; note?: React.ReactNode }) {15 const sp = await searchParams;16 const sort = typeof sp.sort === "string" ? sp.sort : defaultSort;17 const page = Math.max(1, Number(sp.page ?? 1) || 1);18 const q = typeof sp.q === "string" ? sp.q : "";19 const all = typeof sp.all === "string";20 const limit = 100;21 const lists = await Promise.all(22 classes.map((c) => apiEnvelope<InstrumentWithQuote[]>(`/v1/instruments?asset_class=${c}${all ? "" : ""ed=1"}&sort=${encodeURIComponent(sort)}&limit=${limit}&offset=${(page - 1) * limit}${q ? `&q=${encodeURIComponent(q)}` : ""}`)),23 );24 const rows = lists.flatMap((l) => l.data);25 const total = lists.reduce((a, l) => a + Number(l.meta.total ?? 0), 0);26 const pages = Math.max(1, Math.ceil(total / limit));27 const link = (params: Record<string, string | number | undefined>) => {28 const u = new URLSearchParams();29 const merged = { sort, page, q, ...(all ? { all: "1" } : {}), ...params };30 for (const [k, v] of Object.entries(merged)) if (v !== undefined && v !== "" && !(k === "page" && v === 1) && !(k === "sort" && v === defaultSort)) u.set(k, String(v));31 const s = u.toString();32 return s ? `${basePath}?${s}` : basePath;33 };34 return (35 <Page wide>36 <PageHeader kicker={kicker} title={title} lead={lead} />37 <div className="mb-3 flex flex-wrap items-center gap-2">38 {SORTS.map(([s, label]) => (39 <Link key={s} href={link({ sort: s, page: 1 })} className={cx("inline-flex h-8 items-center rounded-full border px-3 text-xs", s === sort ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2 hover:border-rule-strong")}>40 {label}41 </Link>42 ))}43 <form action={basePath} className="ml-auto flex items-center gap-2">44 {sort !== defaultSort && <input type="hidden" name="sort" value={sort} />}45 {all && <input type="hidden" name="all" value="1" />}46 <input name="q" defaultValue={q} placeholder="Filter symbol or name" className="h-9 w-48 rounded-md border border-rule bg-surface px-2 text-sm outline-none focus:border-rule-strong" />47 <Link href={link({ all: all ? undefined : "1", page: 1 })} className="text-xs text-ink-3 hover:text-ink">48 {all ? "Quoted only" : "Include unquoted"}49 </Link>50 </form>51 </div>52 <InstrumentTable rows={rows} liveClass={classes} showExchange={showExchange} showClass={classes.length > 1} defaultSort={sort.replace("-", "") === "change" ? "change" : sort === "volume" ? "volume" : "symbol"} emptyText={q ? `No instruments match “${q}”.` : "No quoted instruments in this class yet."} />53 <div className="mt-3 flex items-center justify-between text-xs text-ink-3">54 <span>55 {total.toLocaleString("en-US")} instrument{total === 1 ? "" : "s"} · page {page} of {pages}56 </span>57 <span className="flex gap-2">58 {page > 1 && (59 <Link href={link({ page: page - 1 })} className="text-accent hover:underline">60 ← Previous61 </Link>62 )}63 {page < pages && (64 <Link href={link({ page: page + 1 })} className="text-accent hover:underline">65 Next →66 </Link>67 )}68 </span>69 </div>70 {note && <p className="mt-4 text-xs text-ink-3">{note}</p>}71 </Page>72 );73}74