Files
investor-flow/app/src/components/FundHoldingsStrip.tsx
T

76 lines
3.0 KiB
TypeScript
Raw Normal View History

"use client";
// Two-tier "who holds this" strip (M22 Symbol Search Index, ADR-0011).
//
// Tier 1 — tracked funds (M21 `fund_position_records`): each fund that holds the
// symbol, with its weight in the fund's disclosed book and the record source/as-of.
// Tier 2 — the count of distinct 13F filers that reported the symbol (M4 cache).
//
// Facts only (ADR-0010 / ADR-0007): never "you should follow this fund".
import { useEffect, useState } from "react";
import { api, type FundHoldersResult } from "@/lib/trpc";
import { Badge } from "@/components/ui";
export function FundHoldingsStrip({ symbol }: { symbol: string | null }) {
const [data, setData] = useState<FundHoldersResult | null>(null);
const [showInstitutions, setShowInstitutions] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
let cancelled = false;
setData(null);
setShowInstitutions(false);
setError(false);
if (!symbol) return;
api.symbols
.holders({ symbol })
.then((d) => { if (!cancelled) setData(d); })
.catch(() => { if (!cancelled) setError(true); });
return () => { cancelled = true; };
}, [symbol]);
if (error) return null; // silent — strip is auxiliary
if (!data) return null;
if (data.tracked.length === 0 && data.institutionCount === 0) return null;
return (
<section className="rounded-xl border border-line-strong bg-surface-raised p-4">
<h3 className="text-sm font-semibold text-fg mb-2">Who holds {symbol}</h3>
{data.tracked.length > 0 && (
<ul className="space-y-1.5">
{data.tracked.map((t) => (
<li key={t.fundId} className="flex items-center gap-2 text-sm">
<span className="font-medium text-fg">{t.fundName}</span>
{t.weightPct != null && (
<span className="text-xs text-fg-muted tnum">{t.weightPct.toFixed(1)}% of book</span>
)}
<Badge tone={t.source === "13f" ? "neutral" : "warn"}>
{t.source === "13f" ? "SEC 13F" : t.source}
</Badge>
<span className="text-[10px] text-fg-faint">as of {t.asOf}</span>
</li>
))}
</ul>
)}
{data.institutionCount > 0 && (
<div className="mt-2 border-t border-line pt-2">
<button
onClick={() => setShowInstitutions((v) => !v)}
className="text-xs text-fg-muted hover:text-fg transition-colors flex items-center gap-1"
>
<span>{data.institutionCount} other institution{data.institutionCount === 1 ? "" : "s"} report holding this (13F)</span>
<span className="text-fg-faint">{showInstitutions ? "▾" : "▸"}</span>
</button>
{showInstitutions && (
<p className="mt-1 text-[10px] text-fg-faint">
From the shared 13F filings cache (M4 institutional module). This is every
distinct filer that reported the symbol, passive and active alike.
</p>
)}
</div>
)}
</section>
);
}