spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1"use client";23import { cx, formatDateTime, formatDuration, relativeTime, STATUS_LABEL, toMs } from "@/lib/format";4import { useLiveQuote, useNow } from "@/lib/stream";5import type { Quote } from "@/lib/types";67/**8 * Human freshness label, ticking every second:9 * REALTIME → "Updated 320 ms ago" · DELAYED → "Delayed 15 min · 12s ago" · AT_CLOSE → "At close · Fri 16:00 ET"10 * END_OF_DAY → "End of day · Sep 11" · STALE → "Stale · last 4h 12m ago" · WITHHELD → "Withheld (rights)".11 */12export function FreshnessLabel({ quote, className, exchangeTz }: { quote: Quote | null | undefined; className?: string; exchangeTz?: string | null }) {13 const now = useNow(1000);14 const live = useLiveQuote(quote?.instrument_id);15 if (!quote) return <span className={cx("text-ink-3", className)}>No observation yet</span>;16 const status = quote.data_status;17 const liveTs = live && live.received >= Date.parse(quote.updated_at) ? live.timestamp : null;18 const srcTs = liveTs ?? toMs(quote.source_timestamp) ?? toMs(quote.updated_at);19 const age = srcTs != null && now ? Math.max(0, now - srcTs) : quote.freshness_ms;20 let text: string;21 switch (status) {22 case "REALTIME":23 text = age == null ? "Live" : age < 60_000 ? `Updated ${formatDuration(age)} ago` : `Live · last ${relativeTime(srcTs, now || Date.now())}`;24 break;25 case "DELAYED":26 text = `Delayed 15 min · ${relativeTime(srcTs, now || Date.now())}`;27 break;28 case "AT_CLOSE":29 text = `At close · ${formatDateTime(srcTs, { tz: exchangeTz ?? "America/New_York" })}`;30 break;31 case "END_OF_DAY":32 text = `End of day · ${formatDateTime(srcTs, { tz: "UTC", dateOnly: true })}`;33 break;34 case "STALE":35 text = `Stale · last update ${relativeTime(srcTs, now || Date.now())}`;36 break;37 case "WITHHELD":38 text = "Value withheld (data rights)";39 break;40 default:41 text = STATUS_LABEL[status] ?? status;42 }43 const tone = status === "REALTIME" ? "text-positive" : status === "STALE" || status === "WITHHELD" ? "text-stale" : status === "DELAYED" ? "text-warning" : "text-ink-3";44 return (45 <span className={cx("inline-flex items-center gap-1.5 whitespace-nowrap text-xs", tone, className)} suppressHydrationWarning>46 {status === "REALTIME" && <span className="live-dot inline-block h-1.5 w-1.5 rounded-full bg-positive" />}47 {text}48 </span>49 );50}5152export function RelativeTime({ value, className }: { value: string | number | null | undefined; className?: string }) {53 const now = useNow(1000);54 const ms = typeof value === "number" ? value : toMs(value);55 return (56 <time className={cx("whitespace-nowrap", className)} dateTime={ms ? new Date(ms).toISOString() : undefined} title={ms ? formatDateTime(ms, { seconds: true }) : undefined} suppressHydrationWarning>57 {ms == null ? "—" : relativeTime(ms, now || Date.now())}58 </time>59 );60}61