SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
4.2 KB · 74 lines tsx
Raw Blame History
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 ? "" : "&quoted=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