import React from 'react'; import { api } from './api.js'; const ROUND_MS = 60_000; function send(roomId, action) { fetch(api('/api/game/control'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roomId, action }), }).catch(() => {}); } function isLandscape() { if (typeof window === 'undefined') return true; return window.matchMedia('(orientation: landscape)').matches; } export default function Controller() { const params = new URLSearchParams(window.location.search); const roomId = (params.get('room') || '').toUpperCase(); const [paired, setPaired] = React.useState(false); const [score, setScore] = React.useState(0); const [timeLeft, setTimeLeft] = React.useState(null); const [running, setRunning] = React.useState(false); const [landscape, setLandscape] = React.useState(isLandscape()); const [armed, setArmed] = React.useState(false); React.useEffect(() => { if (!roomId) return; const es = new EventSource(api(`/api/game/controller-events/${roomId}`)); es.addEventListener('ready', (e) => { try { setPaired(JSON.parse(e.data).paired); } catch {} }); es.addEventListener('paired', () => setPaired(true)); es.addEventListener('state', (e) => { try { const s = JSON.parse(e.data); if (typeof s.score === 'number') setScore(s.score); if (typeof s.timeLeft === 'number') setTimeLeft(s.timeLeft); if (typeof s.running === 'boolean') setRunning(s.running); } catch {} }); es.onerror = () => {}; return () => es.close(); }, [roomId]); React.useEffect(() => { const prev = document.body.style.overscrollBehavior; document.body.style.overscrollBehavior = 'none'; document.body.classList.add('controller-body'); return () => { document.body.style.overscrollBehavior = prev; document.body.classList.remove('controller-body'); }; }, []); React.useEffect(() => { const mq = window.matchMedia('(orientation: landscape)'); const update = () => setLandscape(mq.matches); mq.addEventListener?.('change', update); window.addEventListener('resize', update); return () => { mq.removeEventListener?.('change', update); window.removeEventListener('resize', update); }; }, []); const arm = React.useCallback(async () => { if (armed) return; setArmed(true); try { await document.documentElement.requestFullscreen?.(); } catch {} try { await screen.orientation?.lock?.('landscape'); } catch {} }, [armed]); if (!roomId) { return (
No room.

Open this page by scanning the QR code shown on the laptop.

); } if (!armed) { return (
ROOM {roomId}
EGG-CATCHER
Rotate your phone landscape.
Tap anywhere to enter fullscreen.
); } if (!landscape) { return (
ROTATE TO LANDSCAPE
turn the phone sideways to play
); } // armed + landscape, but game hasn't started → show START screen if (!running) { return (
ROOM {roomId}
{paired ? 'READY TO PLAY' : 'CONNECTING…'}
{paired ? 'Press START to begin the 60-second round.' : 'Linking to the laptop…'}
{score > 0 && (
Last round: {score}
)}
); } const press = (dir) => (e) => { e.preventDefault(); send(roomId, `${dir}:down`); }; const release = (dir) => (e) => { e.preventDefault(); send(roomId, `${dir}:up`); }; const ratio = timeLeft == null ? 1 : Math.max(0, Math.min(1, timeLeft / ROUND_MS)); return (
● {paired ? 'LINKED' : 'WAITING'}
SCORE {String(score).padStart(4, '0')}
ROOM {roomId}
0.4 ? '#5db7e8' : ratio > 0.18 ? '#ffd86b' : '#ff9573', }} />
); }