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 { useMemo } from "react";5import { ConfidenceMeter } from "@/components/ui/confidence";6import { DataTable, type Column } from "@/components/ui/data-table";7import { FreshnessLabel } from "@/components/ui/freshness";8import { LivePrice, ChangeCell } from "@/components/ui/price";9import { StatusBadge } from "@/components/ui/status-badge";10import { ASSET_CLASS_LABEL, formatCompact, instrumentHref } from "@/lib/format";11import { useLiveQuote, useMarketStream } from "@/lib/stream";12import type { InstrumentWithQuote } from "@/lib/types";1314function LiveChange({ row }: { row: InstrumentWithQuote }) {15 const live = useLiveQuote(row.id);16 const useLive = !!live && row.quote && live.received >= Date.parse(row.quote.updated_at);17 return <ChangeCell value={useLive ? live!.change_pct : row.quote?.change_percent} />;18}1920function LiveVolume({ row }: { row: InstrumentWithQuote }) {21 const live = useLiveQuote(row.id);22 const v = live && row.quote && live.received >= Date.parse(row.quote.updated_at) ? live.volume : row.quote?.volume;23 return <span className="mono tnum text-ink-2">{formatCompact(v)}</span>;24}2526/**27 * Dense instrument table with live cells. Subscribes to the per-class stream channel when28 * `liveClass` is given (one subscription for the whole table), otherwise per-instrument channels29 * for tables under ~60 rows.30 */31export function InstrumentTable({ rows, liveClass, showClass, showExchange, defaultSort = "change", compact, maxHeight, emptyText }: { rows: InstrumentWithQuote[]; liveClass?: string | string[]; showClass?: boolean; showExchange?: boolean; defaultSort?: string; compact?: boolean; maxHeight?: string; emptyText?: string }) {32 const channels = useMemo(() => {33 if (liveClass) return (Array.isArray(liveClass) ? liveClass : [liveClass]).map((c) => `quotes:class:${c}`);34 return rows.slice(0, 80).map((r) => `quotes:${r.id}`);35 }, [liveClass, rows]);36 useMarketStream(channels);3738 const columns: Column<InstrumentWithQuote>[] = [39 {40 key: "symbol",41 header: "Symbol",42 sort: (r) => r.symbol,43 cell: (r) => (44 <Link href={instrumentHref(r.id)} className="block min-w-0">45 <span className="mono block font-medium leading-tight">{r.symbol}</span>46 <span className="block max-w-[180px] truncate text-[11.5px] text-ink-3 sm:max-w-[260px]">{r.name}</span>47 </Link>48 ),49 },50 ...(showClass ? [{ key: "class", header: "Class", hideBelow: "sm" as const, sort: (r: InstrumentWithQuote) => r.asset_class, cell: (r: InstrumentWithQuote) => <span className="text-xs text-ink-2">{ASSET_CLASS_LABEL[r.asset_class] ?? r.asset_class}</span> }] : []),51 ...(showExchange ? [{ key: "exchange", header: "Venue", hideBelow: "md" as const, sort: (r: InstrumentWithQuote) => r.exchange_id ?? "", cell: (r: InstrumentWithQuote) => <span className="mono text-xs text-ink-2">{r.mic ?? r.exchange_id?.toUpperCase() ?? "—"}</span> }] : []),52 { key: "price", header: "Price", num: true, sort: (r) => r.quote?.price, cell: (r) => <LivePrice instrumentId={r.id} quote={r.quote} assetClass={r.asset_class} showChange={false} /> },53 { key: "change", header: "Chg %", num: true, sort: (r) => r.quote?.change_percent, cell: (r) => <LiveChange row={r} /> },54 ...(compact ? [] : [{ key: "volume", header: "Volume", num: true, hideBelow: "md" as const, sort: (r: InstrumentWithQuote) => r.quote?.volume, cell: (r: InstrumentWithQuote) => <LiveVolume row={r} /> }]),55 { key: "conf", header: "Confidence", hideBelow: "sm", sort: (r) => r.quote?.confidence, cell: (r) => <ConfidenceMeter confidence={r.quote?.confidence} sources={r.quote?.source_count} proxies={r.quote?.proxy_count} validators={r.quote?.validator_count} /> },56 { key: "status", header: "Status", sort: (r) => r.quote?.data_status, cell: (r) => <StatusBadge status={r.quote?.data_status} /> },57 ...(compact ? [] : [{ key: "fresh", header: "Freshness", hideBelow: "lg" as const, cell: (r: InstrumentWithQuote) => <FreshnessLabel quote={r.quote} /> }]),58 ];59 return <DataTable rows={rows} columns={columns} rowKey={(r) => r.id} defaultSort={defaultSort} maxHeight={maxHeight} emptyText={emptyText} />;60}61