import { useState, useCallback, useEffect, useRef, useMemo } from "react"; import type { LintFinding } from "../components/LintModal"; import { usePlayerStore } from "../player"; interface RawFinding { severity?: string; message?: string; file?: string; fixHint?: string; elementId?: string; selector?: string; code?: string; } function parseFinding(f: RawFinding): LintFinding & { elementId?: string; file?: string } { return { severity: f.severity === "error" ? ("error" as const) : ("warning" as const), message: f.message ?? "", file: f.file, fixHint: f.fixHint, elementId: f.elementId, }; } export function useLintModal(projectId: string | null, refreshKey?: number) { const [lintModal, setLintModal] = useState(null); const [linting, setLinting] = useState(false); const [backgroundFindings, setBackgroundFindings] = useState< Array >([]); const autoLintRanRef = useRef(false); const runLint = useCallback( async (opts?: { background?: boolean }) => { if (!projectId) return; if (!opts?.background) setLinting(true); try { const res = await fetch(`/api/projects/${projectId}/lint`); const data = await res.json(); const parsed = ((data.findings ?? []) as RawFinding[]).map(parseFinding); if (opts?.background) { setBackgroundFindings(parsed); } else { setLintModal(parsed); setBackgroundFindings(parsed); } } catch (err) { if (!opts?.background) { const msg = err instanceof Error ? err.message : String(err); setLintModal([{ severity: "error", message: `Failed to run lint: ${msg}` }]); } } finally { if (!opts?.background) setLinting(false); } }, [projectId], ); const handleLint = useCallback(() => runLint(), [runLint]); const prevProjectIdRef = useRef(projectId); useEffect(() => { if (projectId !== prevProjectIdRef.current) { autoLintRanRef.current = false; prevProjectIdRef.current = projectId; } if (!projectId || autoLintRanRef.current) return; autoLintRanRef.current = true; void runLint({ background: true }); }, [projectId, runLint]); useEffect(() => { if (!projectId || !refreshKey) return; const timer = setTimeout(() => void runLint({ background: true }), 1000); return () => clearTimeout(timer); }, [projectId, refreshKey, runLint]); const closeLintModal = useCallback(() => setLintModal(null), []); const groupFindings = useCallback( (keyFn: (f: (typeof backgroundFindings)[0]) => string | undefined) => { const map = new Map(); for (const f of backgroundFindings) { const key = keyFn(f); if (!key) continue; const prev = map.get(key) ?? { count: 0, messages: [] }; prev.count += 1; prev.messages.push(f.message); map.set(key, prev); } return map; }, [backgroundFindings], ); const findingsByElement = useMemo(() => groupFindings((f) => f.elementId), [groupFindings]); const findingsByFile = useMemo(() => groupFindings((f) => f.file), [groupFindings]); // Sync lint findings directly to the player store — eliminates the // mirroring useEffect that was previously in App.tsx. useEffect(() => { usePlayerStore.getState().setLintFindingsByElement(findingsByElement); }, [findingsByElement]); return { lintModal, linting, handleLint, closeLintModal, backgroundFindings, findingsByElement, findingsByFile, }; }