spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1"use client";23import Link from "next/link";4import { ConfidenceMeter } from "@/components/ui/confidence";5import { FreshnessLabel } from "@/components/ui/freshness";6import { LivePrice } from "@/components/ui/price";7import { StatusBadge } from "@/components/ui/status-badge";8import { ASSET_CLASS_LABEL } from "@/lib/format";9import { useLiveQuote, useMarketStream } from "@/lib/stream";10import type { Exchange, Instrument, InstrumentCoverage, Quote } from "@/lib/types";11import { CoverageBadge } from "@/components/market/coverage-badge";1213/** Sticky quote header: symbol, name, live price + change, status badge, freshness, confidence. */14export function InstrumentHeader({ instrument: i, quote, exchange, coverage }: { instrument: Instrument; quote: Quote | null; exchange: (Exchange & { status?: Exchange["status"] }) | null; coverage?: InstrumentCoverage }) {15 useMarketStream([`quotes:${i.id}`, `events:${i.id}`]);16 const live = useLiveQuote(i.id);17 const sources = live && quote && live.received >= Date.parse(quote.updated_at) ? live.sources : quote?.source_count;18 const conf = live && quote && live.received >= Date.parse(quote.updated_at) ? live.confidence : quote?.confidence;19 return (20 <div className="sticky top-[var(--header-h)] z-[60] border-b border-rule bg-canvas/95 backdrop-blur md:top-[calc(var(--header-h)+29px)]">21 <div className="mx-auto flex max-w-[1440px] flex-wrap items-end justify-between gap-x-6 gap-y-2 px-3 py-3 sm:px-5">22 <div className="min-w-0">23 <div className="flex flex-wrap items-center gap-2 text-xs text-ink-3">24 <Link href={classHref(i.asset_class)} className="hover:text-ink">25 {ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class}26 </Link>27 {exchange && (28 <>29 <span>·</span>30 <Link href={`/exchanges/${exchange.id}`} className="hover:text-ink">31 {exchange.name}32 </Link>33 {exchange.status && <StatusBadge status={exchange.status.state} />}34 </>35 )}36 {i.country && i.country !== "XX" && (37 <>38 <span>·</span>39 <Link href={`/countries/${i.country}`} className="hover:text-ink">40 {i.country}41 </Link>42 </>43 )}44 </div>45 <h1 className="mt-0.5 flex flex-wrap items-baseline gap-x-3">46 <span className="mono text-2xl font-semibold tracking-tight sm:text-3xl">{i.symbol}</span>47 <span className="truncate text-base text-ink-2 sm:text-lg">{i.name}</span>48 </h1>49 </div>50 <div className="flex flex-col items-start gap-1 sm:items-end">51 <div className="flex items-center gap-3">52 <LivePrice instrumentId={i.id} quote={quote} assetClass={i.asset_class} big />53 <StatusBadge status={quote?.data_status ?? "UNKNOWN"} dot />54 </div>55 <div className="flex flex-wrap items-center gap-3">56 <FreshnessLabel quote={quote} exchangeTz={exchange?.timezone} />57 <ConfidenceMeter confidence={conf} sources={sources} proxies={quote?.proxy_count} validators={quote?.validator_count} />58 {coverage && <CoverageBadge coverage={coverage} compact className="hidden sm:inline-flex" />}59 </div>60 </div>61 </div>62 </div>63 );64}6566function classHref(c: string) {67 switch (c) {68 case "EQUITY":69 return "/stocks";70 case "ETF":71 case "ETN":72 return "/etfs";73 case "INDEX":74 return "/indices";75 case "CRYPTO":76 return "/crypto";77 case "FOREX":78 return "/forex";79 case "TREASURY":80 case "BOND":81 case "INTEREST_RATE":82 return "/rates";83 case "COMMODITY":84 case "FUTURE":85 return "/commodities";86 default:87 return "/markets";88 }89}90