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%
3.5 KB · 66 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { InstrumentTable } from "@/components/market/instrument-table";4import { Empty, Page, PageHeader, Section } from "@/components/ui/section";5import { StatusBadge } from "@/components/ui/status-badge";6import { api } from "@/lib/api";7import { ASSET_CLASS_LABEL } from "@/lib/format";8import type { InstrumentWithQuote, SearchResult } from "@/lib/types";910export const metadata: Metadata = { title: "Search", robots: { index: false } };11export const dynamic = "force-dynamic";1213export default async function SearchPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {14  const sp = await searchParams;15  const q = typeof sp.q === "string" ? sp.q.trim() : "";16  const res = q ? await api<SearchResult>(`/v1/search?q=${encodeURIComponent(q)}&limit=50`) : null;17  const groups = res?.groups ?? {};18  const instrumentGroups = Object.entries(groups).filter(([g]) => g !== "EXCHANGE" && g !== "COUNTRY");19  return (20    <Page wide>21      <PageHeader kicker="Search" title={q ? `Results for “${q}”` : "Search"} lead="Symbols, names, aliases, exchanges and countries. Instruments with a canonical quote rank first." />22      <form action="/search" className="mb-6 flex gap-2">23        <input name="q" defaultValue={q} placeholder="AAPL, Bitcoin, S&P 500, TSX, Canada, US 10Y…" className="h-12 flex-1 rounded-md border border-rule bg-surface px-3 text-[15px] outline-none focus:border-rule-strong" autoFocus />24        <button type="submit" className="h-12 rounded-md bg-ink px-4 text-sm font-medium text-canvas">25          Search26        </button>27      </form>28      {q && !res?.results.length && <Empty>No results for “{q}”.</Empty>}29      {(groups.EXCHANGE as Array<Record<string, string>> | undefined)?.length ? (30        <Section title="Exchanges">31          <ul className="grid grid-cols-1 [&>*]:min-w-0 gap-2 sm:grid-cols-2 lg:grid-cols-3">32            {(groups.EXCHANGE as Array<Record<string, string>>).map((e) => (33              <li key={e.id} className="rounded-md border border-rule bg-surface px-3 py-2">34                <Link href={`/exchanges/${e.id}`} className="font-medium hover:underline">35                  {e.name}36                </Link>37                <div className="mt-0.5 flex items-center gap-2 text-xs text-ink-3">38                  <StatusBadge status={e.status} /> {e.mic ?? ""} · {e.country}39                </div>40              </li>41            ))}42          </ul>43        </Section>44      ) : null}45      {(groups.COUNTRY as Array<Record<string, string>> | undefined)?.length ? (46        <Section title="Countries">47          <ul className="flex flex-wrap gap-2">48            {(groups.COUNTRY as Array<Record<string, string>>).map((c) => (49              <li key={c.code}>50                <Link href={`/countries/${c.code}`} className="inline-flex h-9 items-center rounded-md border border-rule bg-surface px-3 text-sm hover:border-rule-strong">51                  {c.name} <span className="mono ml-2 text-xs text-ink-3">{c.code}</span>52                </Link>53              </li>54            ))}55          </ul>56        </Section>57      ) : null}58      {instrumentGroups.map(([g, items]) => (59        <Section key={g} title={ASSET_CLASS_LABEL[g] ?? g} hint={`${(items as unknown[]).length} match${(items as unknown[]).length === 1 ? "" : "es"}`}>60          <InstrumentTable rows={items as InstrumentWithQuote[]} compact showExchange />61        </Section>62      ))}63    </Page>64  );65}66