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%
2.9 KB · 61 lines tsx
Raw Blame History
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