"use client"; import { useCallback, useEffect, useRef, useState } from "react"; type VerseQueueItem = { ref: string; text: string }; type MonitorState = { ref: string; text: string; updatedAt: number; cleared: boolean; operatorQueue: VerseQueueItem[]; queueMode: boolean; }; export default function MonitorPage() { const [authed, setAuthed] = useState(null); const [password, setPassword] = useState(""); const [loginError, setLoginError] = useState(""); const [logging, setLogging] = useState(false); const [state, setState] = useState(null); const [lowerThirds, setLowerThirds] = useState(false); const [showControls, setShowControls] = useState(false); const [showQueue, setShowQueue] = useState(false); const lastUpdatedAt = useRef(0); const intervalRef = useRef | null>(null); const controlsTimerRef = useRef | null>(null); // ─── Auth ──────────────────────────────────────────────────────────────── const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoginError(""); setLogging(true); try { const res = await fetch("/api/monitor/auth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); if (!res.ok) { setLoginError("Incorrect password."); return; } setAuthed(true); startPolling(); } catch { setLoginError("Connection error. Retry."); } finally { setLogging(false); } }; // ─── Polling ───────────────────────────────────────────────────────────── const poll = useCallback(async () => { try { const res = await fetch("/api/monitor/state"); if (res.status === 401) { setAuthed(false); stopPolling(); return; } if (!res.ok) return; const data = await res.json() as MonitorState; if (data.updatedAt !== lastUpdatedAt.current) { lastUpdatedAt.current = data.updatedAt; setState(data); if (data.queueMode && data.operatorQueue.length > 0) setShowQueue(true); } } catch { /* network blip */ } }, []); const stopPolling = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; const startPolling = useCallback(() => { stopPolling(); intervalRef.current = setInterval(() => void poll(), 2000); }, [poll]); useEffect(() => { void (async () => { const res = await fetch("/api/monitor/state"); if (res.ok) { const data = await res.json() as MonitorState; lastUpdatedAt.current = data.updatedAt; setState(data); setAuthed(true); startPolling(); } else { setAuthed(false); } })(); return stopPolling; }, [startPolling]); // ─── Operator actions ───────────────────────────────────────────────────── const operatorGo = async () => { await fetch("/api/monitor/push", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ operatorGo: true }), }); void poll(); }; const operatorSkip = async () => { await fetch("/api/monitor/push", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ operatorSkip: true }), }); void poll(); }; const toggleQueueMode = async (enabled: boolean) => { await fetch("/api/monitor/push", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ setQueueMode: enabled }), }); void poll(); }; // ─── Controls auto-hide ─────────────────────────────────────────────────── const revealControls = () => { setShowControls(true); if (controlsTimerRef.current) clearTimeout(controlsTimerRef.current); controlsTimerRef.current = setTimeout(() => setShowControls(false), 4000); }; // ─── Login screen ───────────────────────────────────────────────────────── if (authed === null) { return (
); } if (!authed) { return (

Nexus Director

Scripture Monitor

Enter the access password to connect

void handleLogin(e)} className="space-y-4"> setPassword(e.target.value)} placeholder="Password" autoFocus className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/25 outline-none focus:border-yellow-400/60 focus:ring-1 focus:ring-yellow-400/20" /> {loginError &&

{loginError}

}
); } // ─── Display screen ──────────────────────────────────────────────────────── const isLive = state && !state.cleared && state.ref; const hasQueue = state?.queueMode && (state.operatorQueue?.length ?? 0) > 0; return (
{/* ── Center layout ─────────────────────────────────────────────── */} {!lowerThirds && (
{isLive ? (

{state.ref}

“{state.text}”

) : (

Nexus Director

{state?.queueMode && (

Queue mode — operator hold active

)}
)}
)} {/* ── Lower-thirds layout ───────────────────────────────────────── */} {lowerThirds && ( <> {/* idle watermark */} {!isLive && (

Nexus Director

)} {/* lower-thirds bar */} {isLive && (
{/* gradient fade */}

{state.ref}

“{state.text}”

)} )} {/* ── Operator queue panel ──────────────────────────────────────── */} {state?.queueMode && showQueue && (

Operator Queue ({state.operatorQueue?.length ?? 0})

{(state.operatorQueue?.length ?? 0) === 0 ? (

No items queued

) : (
{(state.operatorQueue ?? []).map((item, i) => (

{item.ref}

{item.text}

{i === 0 && (
)}
))}
)}
)} {/* ── Control bar (auto-hide) ───────────────────────────────────── */}
{/* Layout toggle */} {/* Queue mode toggle */}
{/* Queue indicator */} {state?.queueMode && (state.operatorQueue?.length ?? 0) > 0 && ( )} {/* Quick Go button (when queue has items) */} {state?.queueMode && (state.operatorQueue?.length ?? 0) > 0 && ( )}
); }