"use client"; import { useEffect, useState } from "react"; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, } from "recharts"; import { api } from "@/lib/api"; import { buildKpis } from "@/lib/overview"; import { KPIStrip } from "@/components/prism/KPIStrip"; import { TickerHeader } from "@/components/prism/TickerHeader"; import type { FilingsResponse, FilingItem, CadencePoint, FormMixRow, TickerOverview, } from "@/types/api"; type FilingsState = "loading" | "ready" | "error" | "empty"; type FormFilter = "all" | "10-K" | "10-Q" | "8-K"; type DateRange = "6m" | "1y" | "all"; const PAGE_SIZE = 25; type Props = { ticker: string; overview: TickerOverview; isSaved: boolean; onToggleWatchlist: () => void; }; // ── Skeleton ────────────────────────────────────────────────────────────────── function SkeletonBlock({ height, className = "" }: { height: number; className?: string }) { return (
); } function FilingsSkeleton() { return ( <>
{[0, 1, 2, 3, 4].map((i) => ( ))}
{[0, 1, 2, 3, 4].map((i) => ( ))}
); } // ── KPI strip ───────────────────────────────────────────────────────────────── function FilingKPIs({ kpis }: { kpis: FilingsResponse["kpis"] }) { const items = [ { label: "TOTAL FILINGS", value: String(kpis.total) }, { label: "10-K ANNUAL", value: String(kpis.count_10k) }, { label: "10-Q QUARTERLY", value: String(kpis.count_10q) }, { label: "8-K EVENTS", value: String(kpis.count_8k) }, { label: "DISTINCT FORMS", value: String(kpis.distinct_forms) }, ]; return (
{items.map((item) => (
{item.label}
{item.value}
))}
); } // ── Cadence chart (Recharts stacked bars) ───────────────────────────────────── const FORM_COLORS: Record = { "10-K": "var(--info, #4A78B5)", "10-Q": "#5B9BD5", "8-K": "var(--caution, #C49545)", other: "var(--fg-3, #8A9AAF)", }; function CadenceChart({ data }: { data: CadencePoint[] }) { if (!data.length) { return (
No filing history available
); } const chartData = data.map((d) => ({ month: d.month.slice(5), // "MM" "10-K": d.count_10k, "10-Q": d.count_10q, "8-K": d.count_8k, Other: d.count_other, })); return ( ); } // ── Form mix table ───────────────────────────────────────────────────────────── function FormMixTable({ rows }: { rows: FormMixRow[] }) { if (!rows.length) return null; return (
Form Count Mix
{rows.map((row) => (
{row.form} {row.count} {row.pct != null ? `${row.pct.toFixed(1)}%` : "—"}
))}
); } // ── Filing badge ─────────────────────────────────────────────────────────────── function FormBadge({ form }: { form: string }) { let bg = "var(--ink-2, #181d26)"; let color = "var(--fg-3)"; let border = "1px solid var(--line-1, #232934)"; if (form === "10-K" || form === "10-Q") { bg = "rgba(74, 120, 181, 0.15)"; color = "var(--info, #4A78B5)"; border = "1px solid rgba(74,120,181,0.3)"; } else if (form === "8-K") { bg = "rgba(196, 149, 69, 0.15)"; color = "var(--caution, #C49545)"; border = "1px solid rgba(196,149,69,0.3)"; } return ( {form} ); } // ── Filing table row ─────────────────────────────────────────────────────────── function FilingRow({ filing }: { filing: FilingItem }) { return (
{filing.date} {filing.title} {filing.url ? ( ) : ( )}
); } // ── Filing table with filters + pagination ───────────────────────────────────── function FilingTable({ filings }: { filings: FilingItem[] }) { const [formFilter, setFormFilter] = useState("all"); const [dateRange, setDateRange] = useState("all"); const [page, setPage] = useState(0); const FORM_FILTERS: { key: FormFilter; label: string }[] = [ { key: "all", label: "ALL" }, { key: "10-K", label: "10-K" }, { key: "10-Q", label: "10-Q" }, { key: "8-K", label: "8-K" }, ]; const DATE_FILTERS: { key: DateRange; label: string }[] = [ { key: "6m", label: "6M" }, { key: "1y", label: "1Y" }, { key: "all", label: "ALL" }, ]; const cutoff: Record = { "6m": new Date(Date.now() - 180 * 86400 * 1000).toISOString().slice(0, 10), "1y": new Date(Date.now() - 365 * 86400 * 1000).toISOString().slice(0, 10), "all": null, }; const filtered = filings.filter((f) => { if (formFilter !== "all" && f.form !== formFilter) return false; const cut = cutoff[dateRange]; if (cut && f.date < cut) return false; return true; }); const totalPages = Math.ceil(filtered.length / PAGE_SIZE); const pageItems = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); // Reset to page 0 when filters change const setFormFilterAndReset = (f: FormFilter) => { setFormFilter(f); setPage(0); }; const setDateRangeAndReset = (r: DateRange) => { setDateRange(r); setPage(0); }; return (
{/* Controls row */}
{FORM_FILTERS.map(({ key, label }) => ( ))}
{DATE_FILTERS.map(({ key, label }) => ( ))}
{/* Table header */}
{["DATE", "FORM", "TITLE", ""].map((h) => ( {h} ))}
{/* Rows */} {pageItems.length === 0 ? (
No filings match selected filters
) : ( pageItems.map((f, i) => ) )} {/* Pagination */} {totalPages > 1 && (
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, filtered.length)} of {filtered.length}
)}
); } // ── Page ────────────────────────────────────────────────────────────────────── export function FilingsPage({ 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 .filings(ticker) .then((res) => { if (cancelled) return; if (!res.filings.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 filings data
)} {state === "empty" && (
No SEC filings found for this ticker
)} {state === "ready" && data && ( <> {/* Two-column: cadence chart + form mix */}
Filing Cadence — Monthly
Form Mix
{/* Readout bar */}
{data.readout}
{/* Filing table */}
All Filings
)}
); }