Massive feature update: Solo session recovery, immediate result saving, image proxy, player reconnection (Live), and UI/UX refinements

This commit is contained in:
2026-04-16 17:53:12 +02:00
parent 247cca0803
commit 4b6e43ba65
8 changed files with 625 additions and 436 deletions
+49 -22
View File
@@ -250,6 +250,8 @@ function LiveSetupPage({ config, token, onSessionCreated }) {
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'session-created') {
// IMPORTANT: Release WS control before handing off
ws.onmessage = null;
onSessionCreated(msg.code, ws, {
code: msg.code,
setName: msg.setName,
@@ -305,7 +307,7 @@ function LiveSetupPage({ config, token, onSessionCreated }) {
}
/* ================================================================== */
/* Live Control Page (FIX: sends admin-join on mount) */
/* Live Control Page */
/* ================================================================== */
function LiveControlPage({ token, code, ws, initialSession }) {
@@ -314,19 +316,31 @@ function LiveControlPage({ token, code, ws, initialSession }) {
const [answerCount, setAnswerCount] = useState(0);
const [result, setResult] = useState(null);
const [leaderboardData, setLeaderboardData] = useState([]);
const refreshRef = useRef(null);
useEffect(() => {
if (!ws) return;
const handler = (e) => {
// Take over the WS completely with onmessage (no addEventListener confusion)
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
console.log('[admin-control] Received:', msg.type);
console.log('[admin-control] Received:', msg.type, msg);
switch (msg.type) {
case 'session-created':
setSession({ code: msg.code, setName: msg.setName, questionCount: msg.questionCount, options: msg.options, players: [], playerCount: 0 });
setSession({
code: msg.code,
setName: msg.setName,
questionCount: msg.questionCount,
options: msg.options,
players: [],
playerCount: 0,
});
setPhase('waiting');
break;
case 'admin-joined':
console.log('[admin-control] Session synced:', msg.session.players, 'count:', msg.session.playerCount);
setSession(msg.session);
setLeaderboardData(msg.session.leaderboard || []);
if (msg.session.status === 'waiting') setPhase('waiting');
@@ -334,69 +348,82 @@ function LiveControlPage({ token, code, ws, initialSession }) {
else if (msg.session.status === 'showing-result') setPhase('result');
else setPhase('question');
break;
case 'player-joined':
case 'player-left':
console.log('[admin-control] Player update:', msg.players, 'count:', msg.playerCount);
setSession((s) => {
if (!s) return { code, players: msg.players, playerCount: msg.playerCount };
return { ...s, players: msg.players, playerCount: msg.playerCount };
const base = s || { code };
return { ...base, players: msg.players, playerCount: msg.playerCount };
});
break;
case 'game-start':
setPhase('active');
break;
case 'question':
setPhase('question');
setAnswerCount(0);
setResult(null);
break;
case 'answer-count':
setAnswerCount(msg.count);
break;
case 'question-result':
setResult(msg);
setLeaderboardData(msg.leaderboard);
setPhase('result');
break;
case 'game-end':
setLeaderboardData(msg.leaderboard);
setPhase('ended');
break;
case 'error':
console.error('Admin error:', msg.message);
console.error('[admin-control] Error:', msg.message);
break;
}
};
ws.addEventListener('message', handler);
// Sync current session state from server
console.log('[admin-control] Sending admin-join for code:', code);
ws.send(JSON.stringify({ type: 'admin-join', token, code }));
// Request current session state from server to sync up
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'admin-join', token, code }));
} else {
const onOpen = () => {
// Safety net: periodically re-sync session state every 5 seconds while waiting
refreshRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'admin-join', token, code }));
ws.removeEventListener('open', onOpen);
};
ws.addEventListener('open', onOpen);
}
}
}, 5000);
return () => ws.removeEventListener('message', handler);
return () => {
clearInterval(refreshRef.current);
ws.onmessage = null;
};
}, [ws, code, token]);
// Stop polling once game starts
useEffect(() => {
if (phase !== 'waiting') {
clearInterval(refreshRef.current);
}
}, [phase]);
const start = () => {
console.log('[admin-control] Sending admin-start');
console.log('[admin-control] Starting game');
ws.send(JSON.stringify({ type: 'admin-start', token, code }));
};
const showResultAction = () => {
console.log('[admin-control] Sending admin-show-result');
ws.send(JSON.stringify({ type: 'admin-show-result', token, code }));
};
const next = () => {
console.log('[admin-control] Sending admin-next');
ws.send(JSON.stringify({ type: 'admin-next', token, code }));
};
const end = () => {
console.log('[admin-control] Sending admin-end');
ws.send(JSON.stringify({ type: 'admin-end', token, code }));
};