spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1"use client";23import { useEffect, useState } from "react";4import { clientApi } from "@/lib/client-api";5import { useNow } from "@/lib/stream";6import { cx } from "@/lib/format";7import type { Exchange } from "@/lib/types";89const CLOCKS: Array<{ id: string; label: string; tz: string }> = [10 { id: "xnys", label: "New York", tz: "America/New_York" },11 { id: "xlon", label: "London", tz: "Europe/London" },12 { id: "xetr", label: "Frankfurt", tz: "Europe/Berlin" },13 { id: "xtks", label: "Tokyo", tz: "Asia/Tokyo" },14 { id: "xhkg", label: "Hong Kong", tz: "Asia/Hong_Kong" },15 { id: "xasx", label: "Sydney", tz: "Australia/Sydney" },16];1718const fmts = new Map<string, Intl.DateTimeFormat>();19const timeIn = (tz: string, now: number) => {20 let f = fmts.get(tz);21 if (!f) {22 f = new Intl.DateTimeFormat("en-GB", { timeZone: tz, hour: "2-digit", minute: "2-digit", hourCycle: "h23" });23 fmts.set(tz, f);24 }25 return f.format(new Date(now));26};2728export const STATE_DOT: Record<string, string> = {29 OPEN: "bg-positive",30 PRE: "bg-warning",31 POST: "bg-warning",32 AUCTION: "bg-warning",33 HALTED: "bg-negative",34 CLOSED: "bg-stale",35 UNKNOWN: "bg-stale",36};3738/** Global clock strip: local time and session state of the main market centres (refreshed every 60 s). */39export function MarketClock({ className }: { className?: string }) {40 const now = useNow(1000);41 const [states, setStates] = useState<Record<string, string>>({});42 useEffect(() => {43 let alive = true;44 const load = () =>45 clientApi<Exchange[]>("/v1/exchanges")46 .then((xs) => alive && setStates(Object.fromEntries(xs.map((x) => [x.id, x.status?.state ?? "UNKNOWN"]))))47 .catch(() => {});48 load();49 const t = setInterval(load, 60_000);50 return () => {51 alive = false;52 clearInterval(t);53 };54 }, []);55 return (56 <div className={cx("flex items-center gap-4 overflow-x-auto whitespace-nowrap text-[11px] text-ink-3 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", className)}>57 {CLOCKS.map((c) => {58 const st = states[c.id] ?? "UNKNOWN";59 return (60 <span key={c.id} className="inline-flex items-center gap-1.5" title={`${c.label}: ${st.toLowerCase()}`}>61 <span className={cx("inline-block h-1.5 w-1.5 rounded-full", STATE_DOT[st] ?? "bg-stale", st === "OPEN" && "live-dot")} />62 <span>{c.label}</span>63 <span className="mono text-ink-2">{now ? timeIn(c.tz, now) : "--:--"}</span>64 </span>65 );66 })}67 </div>68 );69}70