"use client"; import { useMemo, useRef, useState } from "react"; import type { DisplaySummary } from "../../lib/transcripts"; import type { VideoStat } from "../../lib/stats"; import type { ChartConfig, ChartFilters } from "../../lib/chartConfig"; import { aggregate, mergeFilters } from "../../lib/chartAggregate"; import { ChartView } from "./ChartView"; import { useChannelSites } from "./channelSites"; import { useSearchSeries } from "./useSearchSeries"; import { copyPngToClipboard, exportCsv, exportPng } from "./chartExport"; type Props = { config: ChartConfig; stats: readonly VideoStat[]; summaries: DisplaySummary[]; dashboardFilters: ChartFilters; onEdit?: () => void; onRemove?: () => void; }; const btn = "rounded-md border border-border px-2 py-1 text-xs hover:bg-muted disabled:opacity-50"; export function ChartCard({ config, stats, summaries, dashboardFilters, onEdit, onRemove, }: Props) { const ref = useRef(null); const [busy, setBusy] = useState(null); const merged = useMemo( () => ({ ...config, filters: mergeFilters(dashboardFilters, config.filters) }), [config, dashboardFilters], ); // Channel→sites map for groupBy "site" (null outside the hub). See // channelSites.tsx. const channelSites = useChannelSites(); // Both data sources are hooked unconditionally (rules of hooks); the search // hook is inert unless this chart has a search block. const metaData = useMemo( () => (config.search ? null : aggregate(stats, merged, { channelSites })), [stats, merged, config.search, channelSites], ); const searchState = useSearchSeries( merged, config.search ?? { queryTree: "", metric: "matchedVideos" }, stats, summaries, !!config.search, ); const data = metaData ?? searchState.data; const loading = config.search ? searchState.loading : false; const run = async (label: string, fn: () => void | Promise) => { setBusy(label); try { await fn(); } catch (e) { console.error(e); alert(e instanceof Error ? e.message : String(e)); } finally { setBusy(null); } }; return (

{config.title}

{config.search && (

{loading ? "Searching…" : `${searchState.matchedVideos} videos · ${searchState.totalHits} hits`}

)}
{onEdit && ( )} {onRemove && ( )}
); }