// App.jsx — root: estado, Supabase, persistencia local de respaldo, navegación, tweaks. const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React; const STORE_KEY = "quiniela_mundial26_v3"; const ADMIN_PIN_KEY = "quiniela_admin_pin_session"; const QUINIELA_STATE_ID = "mundial_2026"; const SUPABASE_URL = "https://gdanpdhftjdwmkkpqica.supabase.co"; const SUPABASE_PUBLISHABLE_KEY = "sb_publishable_3a5E6lcp3Bl_jBcEai8_HQ_3prZjZH0"; const SUPABASE_CLIENT = window.supabase ? window.supabase.createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY) : null; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": ["#2433FF", "#0B1A8C"], "density": "regular", "ptExact": 5, "ptResult": 3 }/*EDITMODE-END*/; function makeBaseState() { return { results: {}, preds: JSON.parse(JSON.stringify(window.SEED_PREDS || {})), koWinners: {}, }; } function mergePreds(basePreds, savedPreds) { const out = JSON.parse(JSON.stringify(basePreds || {})); Object.keys(savedPreds || {}).forEach((mid) => { out[mid] = { ...(out[mid] || {}), ...(savedPreds[mid] || {}) }; }); return out; } function normalizeState(raw) { const base = makeBaseState(); const s = raw || {}; return { results: s.results || base.results, preds: mergePreds(base.preds, s.preds), koWinners: s.koWinners || s.ko_winners || base.koWinners, updatedAt: s.updatedAt || s.updated_at || null, updatedBy: s.updatedBy || s.updated_by || null, }; } function normalizeRemoteState(row) { return normalizeState({ results: row && row.results, preds: row && row.preds, ko_winners: row && row.ko_winners, updated_at: row && row.updated_at, updated_by: row && row.updated_by, }); } function hasRemoteData(row) { if (!row) return false; return Object.keys(row.results || {}).length > 0 || Object.keys(row.preds || {}).length > 0 || Object.keys(row.ko_winners || {}).length > 0; } function loadState() { const base = makeBaseState(); try { const raw = localStorage.getItem(STORE_KEY); if (raw) return normalizeState(JSON.parse(raw)); } catch (e) {} return base; } const NAV = [ { id: "tabla", label: "Tabla", icon: "table" }, { id: "partidos", label: "Partidos", icon: "matches" }, { id: "mundial", label: "Mundial", icon: "trophy" }, { id: "reglas", label: "Reglas", icon: "info" }, ]; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [state, setState] = useStateA(loadState); const [tab, setTab] = useStateA("tabla"); const [activeMatch, setActiveMatch] = useStateA(null); const [menu, setMenu] = useStateA(false); const [adminPin, setAdminPin] = useStateA(() => { try { return sessionStorage.getItem(ADMIN_PIN_KEY) || ""; } catch (e) { return ""; } }); const [syncStatus, setSyncStatus] = useStateA({ kind: SUPABASE_CLIENT ? "loading" : "error", text: SUPABASE_CLIENT ? "Conectando con Supabase..." : "Supabase no cargó. Revisa la conexión." }); const saveSeq = useRefA(0); const hydrateDone = useRefA(false); useEffectA(() => { try { localStorage.setItem(STORE_KEY, JSON.stringify({ ...state, updatedAt: new Date().toISOString() })); } catch (e) {} }, [state]); useEffectA(() => { const acc = Array.isArray(t.accent) ? t.accent : [t.accent, "#0B1A8C"]; const root = document.documentElement; root.style.setProperty("--accent", acc[0]); root.style.setProperty("--accent-ink", acc[1] || "#0B1A8C"); }, [t.accent]); const scoring = { exact: t.ptExact, result: t.ptResult }; const formatSyncTime = () => { try { return new Intl.DateTimeFormat("es-VE", { timeZone: "America/Caracas", hour: "numeric", minute: "2-digit", }).format(new Date()); } catch (e) { return "ahora"; } }; const ensureAdminPin = () => { const stored = adminPin || (() => { try { return sessionStorage.getItem(ADMIN_PIN_KEY) || ""; } catch (e) { return ""; } })(); if (stored) return stored; const entered = window.prompt("Ingresa el PIN de administrador para guardar en Supabase:"); if (!entered) { setSyncStatus({ kind: "warn", text: "Cambio cancelado: hace falta el PIN de administrador." }); return null; } try { sessionStorage.setItem(ADMIN_PIN_KEY, entered); } catch (e) {} setAdminPin(entered); return entered; }; const clearAdminPin = () => { try { sessionStorage.removeItem(ADMIN_PIN_KEY); } catch (e) {} setAdminPin(""); }; const saveRemote = async (nextState, pin) => { if (!SUPABASE_CLIENT) { setSyncStatus({ kind: "error", text: "No se pudo conectar con Supabase." }); return false; } const seq = ++saveSeq.current; setSyncStatus({ kind: "loading", text: "Guardando en Supabase..." }); const { data, error } = await SUPABASE_CLIENT.rpc("save_quiniela_state", { p_pin: pin, p_results: nextState.results || {}, p_preds: nextState.preds || {}, p_ko_winners: nextState.koWinners || {}, p_updated_by: "app" }); if (error) { if ((error.message || "").includes("invalid_pin")) { clearAdminPin(); window.alert("PIN incorrecto. Vuelve a intentarlo."); setSyncStatus({ kind: "error", text: "No se guardó: PIN incorrecto." }); } else { console.error(error); setSyncStatus({ kind: "error", text: "No se pudo guardar en Supabase. Revisa conexión o permisos." }); } return false; } if (seq === saveSeq.current) { setSyncStatus({ kind: "ok", text: `Sincronizado · ${formatSyncTime()}` }); } if (data) { const row = Array.isArray(data) ? data[0] : data; if (row && row.updated_at) { try { localStorage.setItem(STORE_KEY, JSON.stringify({ ...nextState, updatedAt: row.updated_at, updatedBy: row.updated_by || "app" })); } catch (e) {} } } return true; }; const fetchRemote = async ({ force = false } = {}) => { if (!SUPABASE_CLIENT) return; setSyncStatus({ kind: "loading", text: "Leyendo datos de Supabase..." }); const { data, error } = await SUPABASE_CLIENT .from("quiniela_state") .select("id, results, preds, ko_winners, updated_at, updated_by") .eq("id", QUINIELA_STATE_ID) .single(); if (error) { console.error(error); setSyncStatus({ kind: "error", text: "No se pudieron leer los datos de Supabase." }); return; } if (force || hasRemoteData(data)) { setState(normalizeRemoteState(data)); setSyncStatus({ kind: "ok", text: `Sincronizado · ${formatSyncTime()}` }); } else { setSyncStatus({ kind: "warn", text: "Supabase está vacío. Usa ‘Guardar este dispositivo en Supabase’ para subir los datos actuales." }); } hydrateDone.current = true; }; useEffectA(() => { if (!SUPABASE_CLIENT) return; let alive = true; let channel = null; const init = async () => { await fetchRemote(); if (!alive) return; channel = SUPABASE_CLIENT .channel("quiniela_state_mundial_2026") .on("postgres_changes", { event: "UPDATE", schema: "public", table: "quiniela_state", filter: `id=eq.${QUINIELA_STATE_ID}` }, (payload) => { if (!payload || !payload.new) return; setState(normalizeRemoteState(payload.new)); setSyncStatus({ kind: "ok", text: `Sincronizado · ${formatSyncTime()}` }); }) .subscribe(); }; init(); return () => { alive = false; if (channel) SUPABASE_CLIENT.removeChannel(channel); }; }, []); const updateStateWithAdmin = (updater) => { const pin = ensureAdminPin(); if (!pin) return; setState((s) => { const next = typeof updater === "function" ? updater(s) : updater; saveRemote(next, pin); return next; }); }; const setResult = (mid, obj) => updateStateWithAdmin((s) => ({ ...s, results: { ...s.results, [mid]: obj }, // Si cambia un resultado de grupos, el cuadro y los ganadores manuales pueden dejar de coincidir. koWinners: {}, })); const setPred = (mid, pid, side, val) => updateStateWithAdmin((s) => { const preds = { ...s.preds }; preds[mid] = { ...(preds[mid] || {}) }; preds[mid][pid] = { ...(preds[mid][pid] || {}), [side]: val }; return { ...s, preds }; }); const setKoWinner = (key, side) => updateStateWithAdmin((s) => { let ko = { ...(s.koWinners || {}) }; if (ko[key] === side) delete ko[key]; else ko[key] = side; // Limpia rondas posteriores que dependían de esta selección. ko = window.clearKoDependants ? window.clearKoDependants(ko, key) : ko; return { ...s, koWinners: ko }; }); const manualSave = async () => { const pin = ensureAdminPin(); if (!pin) return; await saveRemote(state, pin); setMenu(false); }; const manualLoad = async () => { await fetchRemote({ force: true }); setMenu(false); }; const askNewPin = () => { const entered = window.prompt("Ingresa el PIN de administrador:", ""); if (!entered) { setMenu(false); return; } try { sessionStorage.setItem(ADMIN_PIN_KEY, entered); } catch (e) {} setAdminPin(entered); setSyncStatus({ kind: "ok", text: "Modo administrador activo en este dispositivo." }); setMenu(false); }; const resetData = () => { const pin = ensureAdminPin(); if (!pin) return; if (!window.confirm("¿Seguro que quieres reiniciar la quiniela en Supabase y en este dispositivo?")) return; const fresh = { results: {}, preds: JSON.parse(JSON.stringify(window.SEED_PREDS || {})), koWinners: {} }; try { localStorage.setItem(STORE_KEY, JSON.stringify(fresh)); } catch (e) {} setState(normalizeState(fresh)); saveRemote(fresh, pin); setMenu(false); }; return (