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 { 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