"use client"; import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api"; import { buildKpis } from "@/lib/overview"; import { fmtLarge, fmtNumber } from "@/lib/format"; import { KPIStrip } from "@/components/prism/KPIStrip"; import { TickerHeader } from "@/components/prism/TickerHeader"; import type { InsidersResponse, InsiderTransaction, TickerOverview } from "@/types/api"; type InsidersState = "loading" | "ready" | "error" | "empty"; type Filter = "all" | "buy" | "sell"; type Props = { ticker: string; overview: TickerOverview; isSaved: boolean; onToggleWatchlist: () => void; }; // ── Skeleton ────────────────────────────────────────────────────────────────── function SkeletonBlock({ height, className = "" }: { height: number; className?: string }) { return (
); } function InsidersSkeleton() { return ( <> {/* KPI row */}
{[0, 1, 2, 3].map((i) => )}
{/* Chart card */}
{/* Table card */}
{[0, 1, 2, 3, 4].map((i) => )}
); } // ── Summary KPI strip ───────────────────────────────────────────────────────── function InsiderKPIs({ summary }: { summary: InsidersResponse["summary"] }) { const items = [ { label: "BUY TRANSACTIONS", value: String(summary.buy_count), tone: "pos" as const }, { label: "TOTAL BOUGHT", value: fmtLarge(summary.buy_value), tone: "pos" as const }, { label: "SELL TRANSACTIONS", value: String(summary.sell_count), tone: "neg" as const }, { label: "TOTAL SOLD", value: fmtLarge(summary.sell_value), tone: "neg" as const }, ]; return (
{items.map((item) => (
{item.label}
{item.value || "—"}
))}
); } // ── Monthly chart (SVG diverging bars) ─────────────────────────────────────── function MonthlyChart({ data }: { data: InsidersResponse["monthly_chart"] }) { const containerRef = useRef(null); const [width, setWidth] = useState(600); useEffect(() => { if (!containerRef.current) return; const ro = new ResizeObserver((entries) => { setWidth(entries[0].contentRect.width); }); ro.observe(containerRef.current); setWidth(containerRef.current.offsetWidth); return () => ro.disconnect(); }, []); if (!data.length) { return (
No activity in the last 6 months
); } const PAD_L = 52; const PAD_R = 16; const PAD_T = 16; const PAD_B = 28; const H = 220; const chartW = Math.max(width - PAD_L - PAD_R, 10); const chartH = H - PAD_T - PAD_B; // max absolute value for symmetric axis const maxVal = Math.max(...data.map((d) => Math.max(d.buy, d.sell)), 0.01); const axisMax = Math.ceil(maxVal * 1.15 * 10) / 10 || 1; const barW = Math.max(Math.floor((chartW / data.length) * 0.6), 4); const barGap = chartW / data.length; const yScale = (v: number) => (v / axisMax) * (chartH / 2); const midY = PAD_T + chartH / 2; // Y axis ticks const ticks = [axisMax, axisMax / 2, 0, -axisMax / 2, -axisMax]; return (
{/* Y axis ticks + labels */} {ticks.map((t) => { const y = midY - yScale(t); const label = t === 0 ? "0" : `${t > 0 ? "+" : ""}${Math.abs(t).toFixed(1)}M`; return ( {label} ); })} {/* Bars */} {data.map((d, i) => { const cx = PAD_L + i * barGap + barGap / 2; const buyH = yScale(d.buy); const sellH = yScale(d.sell); return ( {/* Buy bar (up) */} {d.buy > 0 && ( )} {/* Sell bar (down) */} {d.sell > 0 && ( )} {/* Month label */} {d.month.slice(5)} ); })} {/* Legend */}
{[ { color: "var(--positive)", label: "Buys" }, { color: "var(--negative)", label: "Sells" }, ].map(({ color, label }) => (
{label}
))}
); } // ── Transaction table ───────────────────────────────────────────────────────── function TransactionRow({ tx }: { tx: InsiderTransaction }) { const isBuy = tx.direction === "buy"; const isSell = tx.direction === "sell"; return (
{tx.insider || "—"} {tx.position || "—"}
{tx.direction.toUpperCase()}
{tx.value ? fmtLarge(tx.value) : "—"}
{tx.shares ? `${fmtNumber(tx.shares, 0)} sh` : ""} {tx.date ? ` · ${tx.date}` : ""}
); } function TransactionTable({ transactions }: { transactions: InsiderTransaction[] }) { const [filter, setFilter] = useState("all"); const filtered = filter === "all" ? transactions : transactions.filter((t) => t.direction === filter); const FILTERS: { key: Filter; label: string }[] = [ { key: "all", label: "ALL" }, { key: "buy", label: "BUYS" }, { key: "sell", label: "SELLS" }, ]; return (
{/* Filter toggles */}
{FILTERS.map(({ key, label }) => ( ))}
{filtered.length === 0 ? (
No transactions match filter
) : ( filtered.map((tx, i) => ) )}
); } // ── Page ────────────────────────────────────────────────────────────────────── export function InsidersPage({ ticker, overview, isSaved, onToggleWatchlist }: Props) { const [data, setData] = useState(null); const [state, setState] = useState("loading"); const kpis = buildKpis(overview); useEffect(() => { let cancelled = false; setState("loading"); setData(null); api .insiders(ticker) .then((res) => { if (cancelled) return; if (!res.transactions.length) { setState("empty"); } else { setData(res); setState("ready"); } }) .catch(() => { if (!cancelled) setState("error"); }); return () => { cancelled = true; }; }, [ticker]); return ( <>
{state === "loading" && } {state === "error" && (
Failed to load insider data
)} {state === "empty" && (
No insider activity reported
)} {state === "ready" && data && ( <>
Monthly Insider Activity — Last 6 Months ($M)
All Transactions
)}
); }