"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 (
{/* 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)
>
)}
>
);
}