spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1"use client";23import { useEffect, useRef, useState } from "react";4import { cx, formatChange, formatPercent, formatQuoteValue } from "@/lib/format";5import { useLiveQuote } from "@/lib/stream";6import type { Quote } from "@/lib/types";78/** Change colour helpers — colour is supplementary; the sign is always in the text. */9export const toneOf = (v: number | null | undefined) => (v == null || !Number.isFinite(v) || v === 0 ? "text-ink-2" : v > 0 ? "text-positive" : "text-negative");1011/**12 * Price cell that merges the server-rendered quote with the live stream (when the instrument is13 * subscribed by an ancestor). Flashes subtly on change. Never shows a live value when the14 * stream quote is older than the server snapshot.15 */16export function LivePrice({ instrumentId, quote, assetClass, className, showChange = true, big = false }: { instrumentId: string; quote: Quote | null | undefined; assetClass?: string | null; className?: string; showChange?: boolean; big?: boolean }) {17 const live = useLiveQuote(instrumentId);18 const serverTs = quote ? Date.parse(quote.updated_at) : 0;19 const useLive = !!live && live.received >= serverTs && live.price != null;20 const price = useLive ? live.price : quote?.price ?? null;21 const change = useLive ? live.change : quote?.change ?? null;22 const pct = useLive ? live.change_pct : quote?.change_percent ?? null;23 const currency = useLive ? live.currency : quote?.currency;24 const prev = useRef<number | null>(null);25 const [flash, setFlash] = useState<"" | "flash-up" | "flash-down">("");26 useEffect(() => {27 if (price == null) return;28 if (prev.current != null && price !== prev.current) {29 setFlash(price > prev.current ? "flash-up" : "flash-down");30 const t = setTimeout(() => setFlash(""), 650);31 prev.current = price;32 return () => clearTimeout(t);33 }34 prev.current = price;35 }, [price]);36 if (price == null) return <span className={cx("mono text-ink-3", className)}>—</span>;37 return (38 <span className={cx("mono inline-flex items-baseline gap-2 rounded-[3px] px-0.5 tnum", flash, className)}>39 <span className={cx(big ? "text-3xl font-semibold tracking-tight sm:text-4xl" : "font-medium")}>{formatQuoteValue(price, assetClass, currency)}</span>40 {showChange && (41 <span className={cx(toneOf(pct), big ? "text-sm" : "text-xs")}>42 {big && change != null ? `${formatChange(change, assetClass)} ` : ""}43 {formatPercent(pct)}44 </span>45 )}46 </span>47 );48}4950export function ChangeCell({ value, className }: { value: number | null | undefined; className?: string }) {51 return <span className={cx("mono tnum", toneOf(value), className)}>{formatPercent(value)}</span>;52}53