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.6 KB · 88 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { EventRow } from "@/components/market/event-row";4import { Empty, Page, PageHeader } from "@/components/ui/section";5import { api } from "@/lib/api";6import { cx, EVENT_TYPE_LABEL } from "@/lib/format";7import type { MarketEvent } from "@/lib/types";89export const metadata: Metadata = { title: "Events", description: "Canonical market events: halts, filings, price moves, volatility, market opens and source incidents.", alternates: { canonical: "/events" } };10export const dynamic = "force-dynamic";1112const TYPES = ["PRICE_CHANGE", "SESSION_HIGH", "SESSION_LOW", "VOLATILITY_SPIKE", "TRADING_HALT", "TRADING_RESUME", "FILING_PUBLISHED", "MARKET_OPEN", "MARKET_CLOSE", "SOURCE_DIVERGENCE", "SOURCE_FAILURE", "SCHEMA_DRIFT", "DOCUMENT_CHANGED"];13const SEVS = ["INFO", "NOTICE", "WARNING", "CRITICAL"];1415export default async function EventsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {16  const sp = await searchParams;17  const type = typeof sp.type === "string" ? sp.type : "";18  const severity = typeof sp.severity === "string" ? sp.severity : "";19  const instrument = typeof sp.instrument === "string" ? sp.instrument : "";20  const country = typeof sp.country === "string" ? sp.country : "";21  const before = typeof sp.before === "string" ? sp.before : "";22  const q = new URLSearchParams({ limit: "100" });23  if (type) q.set("type", type);24  if (severity) q.set("severity", severity);25  if (instrument) q.set("instrument", instrument);26  if (country) q.set("country", country);27  if (before) q.set("before", before);28  const events = await api<MarketEvent[]>(`/v1/events?${q}`);29  const link = (patch: Record<string, string>) => {30    const u = new URLSearchParams();31    for (const [k, v] of Object.entries({ type, severity, instrument, country, ...patch })) if (v) u.set(k, v);32    const s = u.toString();33    return s ? `/events?${s}` : "/events";34  };35  const last = events[events.length - 1];36  const lastTs = last ? (typeof last.timestamp === "number" ? new Date(last.timestamp).toISOString() : String(last.timestamp).replace(" ", "T")) : null;37  return (38    <Page wide>39      <PageHeader kicker="Events" title="Market events" lead="Deduplicated, source-attributed events derived from observations (price moves, session extremes, volatility), venue feeds (halts), regulators (filings) and Market Atlas' own monitoring (source incidents)." actions={<Link href="/live" className="text-sm text-accent hover:underline">Watch live →</Link>} />40      <div className="mb-3 flex flex-wrap gap-1.5">41        <Link href={link({ type: "" })} className={cx("inline-flex h-8 items-center rounded-full border px-3 text-xs", !type ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2 hover:border-rule-strong")}>42          All types43        </Link>44        {TYPES.map((t) => (45          <Link key={t} href={link({ type: t })} className={cx("inline-flex h-8 items-center rounded-full border px-3 text-xs", type === t ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2 hover:border-rule-strong")}>46            {EVENT_TYPE_LABEL[t] ?? t}47          </Link>48        ))}49      </div>50      <div className="mb-4 flex flex-wrap items-center gap-1.5 text-xs">51        <span className="text-ink-3">Severity:</span>52        <Link href={link({ severity: "" })} className={cx("rounded-full border px-2.5 py-1", !severity ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2")}>53          any54        </Link>55        {SEVS.map((s) => (56          <Link key={s} href={link({ severity: s })} className={cx("rounded-full border px-2.5 py-1", severity === s ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2")}>57            {s.toLowerCase()}58          </Link>59        ))}60        {(instrument || country) && (61          <span className="ml-2 text-ink-3">62            filtered by {instrument ? `instrument ${instrument}` : `country ${country}`} ·{" "}63            <Link href={link({ instrument: "", country: "" })} className="text-accent hover:underline">64              clear65            </Link>66          </span>67        )}68      </div>69      {events.length ? (70        <ul className="rounded-md border border-rule bg-surface px-3">71          {events.map((e) => (72            <EventRow key={e.id} e={e} />73          ))}74        </ul>75      ) : (76        <Empty>No events match these filters.</Empty>77      )}78      {events.length >= 100 && lastTs && (79        <div className="mt-3 text-right text-xs">80          <Link href={link({ before: lastTs })} className="text-accent hover:underline">81            Older →82          </Link>83        </div>84      )}85    </Page>86  );87}88