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.4 KB · 77 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { WorldMap } from "@/components/market/world-map";4import { ChangeCell } from "@/components/ui/price";5import { Page, PageHeader, Section } from "@/components/ui/section";6import { StatusBadge } from "@/components/ui/status-badge";7import { api } from "@/lib/api";8import type { Breadth, Exchange, ExchangeStatus } from "@/lib/types";910type Row = Exchange & { status: ExchangeStatus; instruments_tracked: number; instruments_quoted: number; breadth: Breadth | null };1112export const metadata: Metadata = { title: "Exchanges", description: "World exchanges with live session state, local time, instruments tracked and breadth.", alternates: { canonical: "/exchanges" } };13export const dynamic = "force-dynamic";1415const ORDER = ["OPEN", "PRE", "POST", "AUCTION", "HALTED", "CLOSED", "UNKNOWN"];1617export default async function ExchangesPage() {18  const rows = await api<Row[]>("/v1/exchanges");19  const sorted = [...rows].sort((a, b) => ORDER.indexOf(a.status.state) - ORDER.indexOf(b.status.state) || b.instruments_tracked - a.instruments_tracked || a.name.localeCompare(b.name));20  const open = rows.filter((r) => r.status.state === "OPEN").length;21  const mapRows = rows.map((r) => ({ id: r.id, name: r.name, mic: r.mic, country: r.country, city: r.city, timezone: r.timezone, lat: r.lat, lon: r.lon, status: r.status, asset_classes: r.asset_classes ?? [], breadth: r.breadth }));22  return (23    <Page wide>24      <PageHeader kicker="World" title="Exchanges" lead={`${rows.length} venues in the atlas · ${open} trading right now. Session states come from the market-hours engine (time zones, holidays, early closes); instrument counts are what Market Atlas tracks, not the venue's full listing.`} />25      <WorldMap exchanges={mapRows} />26      <Section title="All venues" hint="sorted by session state, then coverage">27        <div className="overflow-x-auto rounded-md border border-rule bg-surface">28          <table className="table-dense">29            <thead>30              <tr>31                <th>Exchange</th>32                <th>MIC</th>33                <th>Country</th>34                <th>State</th>35                <th>Local time</th>36                <th className="hidden md:table-cell">Next</th>37                <th className="text-right">Tracked</th>38                <th className="text-right">Quoted</th>39                <th className="hidden text-right lg:table-cell">Adv / Dec</th>40                <th className="hidden text-right lg:table-cell">Median</th>41              </tr>42            </thead>43            <tbody>44              {sorted.map((r) => (45                <tr key={r.id}>46                  <td>47                    <Link href={`/exchanges/${r.id}`} className="font-medium hover:underline">48                      {r.name}49                    </Link>50                    <span className="block text-[11px] text-ink-3">{r.city ?? r.operator ?? ""}</span>51                  </td>52                  <td className="mono text-ink-2">{r.mic ?? "—"}</td>53                  <td>54                    <Link href={`/countries/${r.country}`} className="hover:underline">55                      {r.country}56                    </Link>57                  </td>58                  <td>59                    <StatusBadge status={r.status.state} />60                    {r.status.isHoliday && r.status.holidayName && <span className="ml-1 text-[11px] text-ink-3">{r.status.holidayName}</span>}61                  </td>62                  <td className="mono text-ink-2">{r.status.localTime}</td>63                  <td className="mono hidden text-xs text-ink-3 md:table-cell">{r.status.nextTransition ? `${r.status.nextTransition.state.toLowerCase()} · ${new Date(r.status.nextTransition.at).toLocaleString("en-GB", { timeZone: r.timezone, weekday: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" })}` : r.sessions.continuous ? "24/7" : "—"}</td>64                  <td className="num">{r.instruments_tracked.toLocaleString("en-US")}</td>65                  <td className="num">{r.instruments_quoted}</td>66                  <td className="num hidden lg:table-cell">{r.breadth ? `${r.breadth.advancers} / ${r.breadth.decliners}` : "—"}</td>67                  <td className="num hidden lg:table-cell">{r.breadth ? <ChangeCell value={r.breadth.median_change_percent} /> : "—"}</td>68                </tr>69              ))}70            </tbody>71          </table>72        </div>73      </Section>74    </Page>75  );76}77