"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} />;
}