import { useState, useCallback, useEffect, useRef } from 'react'; import { getProgress, updateProgress, resetProgressTopic } from '../lib/api'; function calcPercentage(row) { if (!row || row.exercises_done === 0) return 0; return Math.round((row.exercises_correct / row.exercises_done) * 100); } export default function useProgress() { const [progress, setProgress] = useState([]); const [error, setError] = useState(null); const wsRef = useRef(null); const reconnectDelayRef = useRef(1000); const reconnectTimerRef = useRef(null); const refresh = useCallback(async () => { try { const rows = await getProgress(); setProgress( rows.map((r) => ({ ...r, percentage: calcPercentage(r), })) ); setError(null); } catch (err) { console.error('[useProgress] refresh error:', err.message); setError(err.message); } }, []); useEffect(() => { refresh(); const connect = () => { const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { reconnectDelayRef.current = 1000; }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'progress_update' && msg.data) { setProgress((prev) => { const row = msg.data; const enriched = { ...row, percentage: calcPercentage(row) }; const idx = prev.findIndex((p) => p.topic === row.topic); if (idx >= 0) { const next = [...prev]; next[idx] = enriched; return next; } return [...prev, enriched]; }); } } catch (e) { // ignore malformed messages } }; ws.onclose = () => { wsRef.current = null; const delay = Math.min(reconnectDelayRef.current, 30000); reconnectDelayRef.current *= 2; reconnectTimerRef.current = setTimeout(connect, delay); }; ws.onerror = () => { ws.close(); }; }; connect(); return () => { if (wsRef.current) { wsRef.current.onclose = null; wsRef.current.close(); wsRef.current = null; } if (reconnectTimerRef.current) { clearTimeout(reconnectTimerRef.current); reconnectTimerRef.current = null; } }; }, [refresh]); const updateExercise = useCallback( async (topic, correct) => { try { const row = await updateProgress(topic, correct); setProgress((prev) => { const idx = prev.findIndex((p) => p.topic === topic); const enriched = { ...row, percentage: calcPercentage(row) }; if (idx >= 0) { const next = [...prev]; next[idx] = enriched; return next; } return [...prev, enriched]; }); setError(null); } catch (err) { console.error('[useProgress] update error:', err.message); setError(err.message); } }, [] ); const resetTopic = useCallback(async (topic) => { try { await resetProgressTopic(topic); setProgress((prev) => prev.filter((p) => p.topic !== topic)); setError(null); } catch (err) { console.error('[useProgress] reset error:', err.message); setError(err.message); } }, []); return { progress, error, refresh, updateExercise, resetTopic, }; }