"use client"; import Link from "next/link"; import { useMemo } from "react"; import { ConfidenceMeter } from "@/components/ui/confidence"; import { DataTable, type Column } from "@/components/ui/data-table"; import { FreshnessLabel } from "@/components/ui/freshness"; import { LivePrice, ChangeCell } from "@/components/ui/price"; import { StatusBadge } from "@/components/ui/status-badge"; import { ASSET_CLASS_LABEL, formatCompact, instrumentHref } from "@/lib/format"; import { useLiveQuote, useMarketStream } from "@/lib/stream"; import type { InstrumentWithQuote } from "@/lib/types"; function LiveChange({ row }: { row: InstrumentWithQuote }) { const live = useLiveQuote(row.id); const useLive = !!live && row.quote && live.received >= Date.parse(row.quote.updated_at); return ; } function LiveVolume({ row }: { row: InstrumentWithQuote }) { const live = useLiveQuote(row.id); const v = live && row.quote && live.received >= Date.parse(row.quote.updated_at) ? live.volume : row.quote?.volume; return {formatCompact(v)}; } /** * Dense instrument table with live cells. Subscribes to the per-class stream channel when * `liveClass` is given (one subscription for the whole table), otherwise per-instrument channels * for tables under ~60 rows. */ export 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 }) { const channels = useMemo(() => { if (liveClass) return (Array.isArray(liveClass) ? liveClass : [liveClass]).map((c) => `quotes:class:${c}`); return rows.slice(0, 80).map((r) => `quotes:${r.id}`); }, [liveClass, rows]); useMarketStream(channels); const columns: Column[] = [ { key: "symbol", header: "Symbol", sort: (r) => r.symbol, cell: (r) => ( {r.symbol} {r.name} ), }, ...(showClass ? [{ key: "class", header: "Class", hideBelow: "sm" as const, sort: (r: InstrumentWithQuote) => r.asset_class, cell: (r: InstrumentWithQuote) => {ASSET_CLASS_LABEL[r.asset_class] ?? r.asset_class} }] : []), ...(showExchange ? [{ key: "exchange", header: "Venue", hideBelow: "md" as const, sort: (r: InstrumentWithQuote) => r.exchange_id ?? "", cell: (r: InstrumentWithQuote) => {r.mic ?? r.exchange_id?.toUpperCase() ?? "—"} }] : []), { key: "price", header: "Price", num: true, sort: (r) => r.quote?.price, cell: (r) => }, { key: "change", header: "Chg %", num: true, sort: (r) => r.quote?.change_percent, cell: (r) => }, ...(compact ? [] : [{ key: "volume", header: "Volume", num: true, hideBelow: "md" as const, sort: (r: InstrumentWithQuote) => r.quote?.volume, cell: (r: InstrumentWithQuote) => }]), { key: "conf", header: "Confidence", hideBelow: "sm", sort: (r) => r.quote?.confidence, cell: (r) => }, { key: "status", header: "Status", sort: (r) => r.quote?.data_status, cell: (r) => }, ...(compact ? [] : [{ key: "fresh", header: "Freshness", hideBelow: "lg" as const, cell: (r: InstrumentWithQuote) => }]), ]; return r.id} defaultSort={defaultSort} maxHeight={maxHeight} emptyText={emptyText} />; }