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 (

Profile

{tab === "account" && (
Handle
setNewHandle(e.target.value)} /> {errHandle &&
{errHandle}
}
Change PIN
setCurrentPin(e.target.value.replace(/[^0-9]/g, ""))} maxLength={4} /> setNewPin(e.target.value.replace(/[^0-9]/g, ""))} maxLength={4} /> {errPin &&
{errPin}
}
Reaction Display Name
Shown when you react to a bet on the board
{ setReactionName(e.target.value); localStorage.setItem("bb_reactor_handle", e.target.value); }} />
)} {tab === "display" && (
Changes sync to Pi in ~2s {syncStatus === "syncing" ? "syncing…" : syncStatus === "synced" ? "✓ synced" : syncStatus === "offline" ? "offline" : "."}
{/* Power + Wife Mode — single tappable rows, current state shown as text on right */}
{/* Brightness */}
Brightness{displayPrefs.brightness}%
setPref("brightness", Number(e.target.value))} className="w-full" />
{/* Speed — three clear controls */}
Speed
Vertical scroll speed{displayPrefs.scroll_speed}
setPref("scroll_speed", Number(e.target.value))} className="w-full" />
SlowHow fast pages scroll upFast
Horizontal scroll speed{displayPrefs.reaction_speed}
setPref("reaction_speed", Number(e.target.value))} className="w-full" />
SlowBottom ticker speedFast
Page change speed{speedFromHold(displayPrefs.page_hold)}
setPref("page_hold", holdFromSpeed(Number(e.target.value)))} className="w-full" />
SlowHow long each screen showsFast
{/* Sections */}
Screens shown
{[["show_wagers","Wagers"],["show_board","Board"],["show_results","Results"],["show_reactions","Reaction ticker"]].map(([k, label]) => { const on = displayPrefs[k] !== false; return ( ); })}
{/* Rotation */}
Rotation
{["0","90","180","270"].map((r) => { const val = r === "0" ? "" : `Rotate:${r}`; return ( ); })}
)}
); } window.ProfileModal = ProfileModal;