const { useState, useEffect, useRef } = React; const DISPLAY_PREF_KEY = "bb_display_prefs"; function loadDisplayPrefs() { try { return JSON.parse(localStorage.getItem(DISPLAY_PREF_KEY) || "{}"); } catch { return {}; } } function ProfileModal({ apiBase, token, visible, onClose, onHandleChange }) { const [tab, setTab] = useState("account"); const [newHandle, setNewHandle] = useState(""); const [currentPin, setCurrentPin] = useState(""); const [newPin, setNewPin] = useState(""); const [errHandle, setErrHandle] = useState(""); const [errPin, setErrPin] = useState(""); const [busyHandle, setBusyHandle] = useState(false); const [busyPin, setBusyPin] = useState(false); const [reactionName, setReactionName] = useState(() => localStorage.getItem("bb_reactor_handle") || ""); const [displayPrefs, setDisplayPrefs] = useState(() => ({ display_enabled: true, wife_mode: false, brightness: 60, scroll_speed: 10, reaction_speed: 8, page_hold: 2.0, page_gap: 0.15, page_label_hold: 1.2, rotation: "Rotate:180", show_wagers: true, show_board: true, show_results: true, show_finals: true, show_reactions: true, ...loadDisplayPrefs(), })); const [syncStatus, setSyncStatus] = useState(""); const syncTimer = useRef(null); const mountedRef = useRef(false); const setPref = (k, v) => setDisplayPrefs((p) => ({ ...p, [k]: v })); // "Page change speed" is presented 1 (slow) .. 10 (fast) but stored as a hold // time in seconds (higher seconds = slower). Map between the two. const holdFromSpeed = (s) => Math.round((5.0 - (s - 1) * 0.5) * 10) / 10; // 1->5.0s, 10->0.5s const speedFromHold = (h) => Math.min(10, Math.max(1, Math.round((5.0 - Number(h)) / 0.5) + 1)); const pushPrefs = async (prefs) => { localStorage.setItem(DISPLAY_PREF_KEY, JSON.stringify(prefs)); const deviceId = (localStorage.getItem("bb_handle") || "").toLowerCase() || "pi-main"; const groupCode = localStorage.getItem("bb_group_code") || ""; try { const r = await fetch(`${apiBase}/display/devices/${encodeURIComponent(deviceId)}/config`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ ...prefs, group_code: groupCode }), }); setSyncStatus(r.ok ? "synced" : "offline"); } catch { setSyncStatus("offline"); } setTimeout(() => setSyncStatus(""), 2500); }; // Auto-push every time prefs change, debounced 450ms useEffect(() => { if (!mountedRef.current) { mountedRef.current = true; return; } setSyncStatus("syncing"); clearTimeout(syncTimer.current); syncTimer.current = setTimeout(() => pushPrefs(displayPrefs), 450); return () => clearTimeout(syncTimer.current); }, [displayPrefs]); const overlayRef = useRef(null); // Non-passive touchmove on overlay prevents page scroll on iOS (React onTouchMove is passive) useEffect(() => { const el = overlayRef.current; if (!el || !visible) return; const trap = (e) => { // Allow scroll inside the card; block everything else from reaching the page if (!e.target.closest('[data-modal-scroll]')) e.preventDefault(); }; el.addEventListener('touchmove', trap, { passive: false }); return () => el.removeEventListener('touchmove', trap); }, [visible]); if (!visible) return null; const submitHandle = async (e) => { e.preventDefault(); setErrHandle(""); const h = newHandle.trim(); if (!h) { setErrHandle("Handle required"); return; } setBusyHandle(true); try { const r = await fetch(`${apiBase}/profile/handle`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ handle: h }), }); const j = await r.json().catch(() => ({})); if (!r.ok || j.ok === false) throw new Error(j.error || `HTTP ${r.status}`); localStorage.setItem("bb_handle", h); if (onHandleChange) onHandleChange(j.handle || h); setNewHandle(""); } catch (e) { setErrHandle(e.message || "Failed"); } finally { setBusyHandle(false); } }; const submitPin = async (e) => { e.preventDefault(); setErrPin(""); if (!/^[0-9]{4}$/.test(newPin)) { setErrPin("PIN must be exactly 4 digits"); return; } setBusyPin(true); try { const r = await fetch(`${apiBase}/profile/pin`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ current: currentPin, pin: newPin }), }); const j = await r.json().catch(() => ({})); if (!r.ok || j.ok === false) throw new Error(j.error || `HTTP ${r.status}`); setCurrentPin(""); setNewPin(""); } catch (e) { setErrPin(e.message || "Failed"); } finally { setBusyPin(false); } }; const TAB = "px-3 py-1.5 rounded text-sm font-medium transition-colors"; const activeTab = TAB + " btn"; const inactiveTab = TAB + " btn-alt"; return (