spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
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