diff --git a/BASEROW-SETUP.md b/BASEROW-SETUP.md index 839ef2c..9850d9f 100644 --- a/BASEROW-SETUP.md +++ b/BASEROW-SETUP.md @@ -28,17 +28,17 @@ Diese Tabelle speichert ALLE Ergebnisse (Live + Solo). Es gibt nur eine einzige. 2. Lösche die automatisch erstellten Beispiel-Spalten 3. Erstelle folgende Felder: -| Nr. | Feldname | Feldtyp | Optionen / Hinweis | -|-----|----------------|---------------------|---------------------------------------------| -| 1 | `Nutzername` | Single line text | | -| 2 | `Frage_ID` | Number (Integer) | | -| 3 | `Fragenset` | Single line text | | -| 4 | `Antwort` | Single line text | Enthält z.B. `A,C` oder `Berlin` | -| 5 | `Richtig` | Boolean | | -| 6 | `Punkte` | Number (Integer) | | -| 7 | `Session_ID` | Single line text | z.B. `LIVE-M3K7X` oder `SOLO-K8P2Q` | -| 8 | `Modus` | Single select | Optionen anlegen: `Live`, `Solo` | -| 9 | `Zeitstempel` | Date | ✅ "Include time" aktivieren | +| Nr. | Feldname | Feldtyp | Optionen / Hinweis | +|-----|---------------|------------------|-------------------------------------| +| 1 | `Nutzername` | Single line text | | +| 2 | `Frage_ID` | Number (Integer) | | +| 3 | `Fragenset` | Single line text | | +| 4 | `Antwort` | Single line text | Enthält z.B. `A,C` oder `Berlin` | +| 5 | `Richtig` | Boolean | | +| 6 | `Punkte` | Number (Integer) | | +| 7 | `Session_ID` | Single line text | z.B. `LIVE-M3K7X` oder `SOLO-K8P2Q` | +| 8 | `Modus` | Single select | Optionen anlegen: `Live`, `Solo` | +| 9 | `Zeitstempel` | Date | ✅ "Include time" aktivieren | 1. **Table-ID notieren**: Klicke auf die Tabelle → die URL enthält `/database/TABLE_ID/...` @@ -55,17 +55,17 @@ Für jedes Fragenset wird eine eigene Tabelle erstellt. Der Aufbau ist immer ide 3. Lösche die automatisch erstellten Beispiel-Spalten 4. Erstelle folgende Felder: -| Nr. | Feldname | Feldtyp | Optionen / Hinweis | -|-----|-------------------|---------------------|------------------------------------------------------------| -| 1 | `Frage` | Long text | Der vollständige Fragetext | -| 2 | `Typ` | Single select | Optionen anlegen: `MC`, `Wahr/Falsch`, `Freitext` | -| 3 | `Bild` | File | Optional — Bild zur Frage hochladen | -| 4 | `Antwort A` | Single line text | Bei Wahr/Falsch: `Wahr` | -| 5 | `Antwort B` | Single line text | Bei Wahr/Falsch: `Falsch` | -| 6 | `Antwort C` | Single line text | Leer lassen bei Wahr/Falsch oder 2 Antworten | -| 7 | `Antwort D` | Single line text | Leer lassen bei weniger als 4 Antworten | -| 8 | `Richtige Antwort`| Single line text | Siehe Ausfüllregeln unten | -| 9 | `Kategorie` | Single select | Optional — z.B. `Geografie`, `Technik`, `Recht` | +| Nr. | Feldname | Feldtyp | Optionen / Hinweis | +|-----|--------------------|------------------|---------------------------------------------------| +| 1 | `Frage` | Long text | Der vollständige Fragetext | +| 2 | `Typ` | Single select | Optionen anlegen: `MC`, `Wahr/Falsch`, `Freitext` | +| 3 | `Bild` | File | Optional — Bild zur Frage hochladen | +| 4 | `Antwort A` | Single line text | Bei Wahr/Falsch: `Wahr` | +| 5 | `Antwort B` | Single line text | Bei Wahr/Falsch: `Falsch` | +| 6 | `Antwort C` | Single line text | Leer lassen bei Wahr/Falsch oder 2 Antworten | +| 7 | `Antwort D` | Single line text | Leer lassen bei weniger als 4 Antworten | +| 8 | `Richtige Antwort` | Single line text | Siehe Ausfüllregeln unten | +| 9 | `Kategorie` | Single select | Optional — z.B. `Geografie`, `Technik`, `Recht` | 1. **Table-ID notieren**: URL enthält `/database/TABLE_ID/...` @@ -73,12 +73,12 @@ Für jedes Fragenset wird eine eigene Tabelle erstellt. Der Aufbau ist immer ide ## 4. Ausfüllregeln für "Richtige Antwort" -| Fragetyp | Beispiel-Wert | Erklärung | -|------------------|----------------------|---------------------------------------------------| -| MC (1 richtig) | `A` | Nur der Buchstabe der richtigen Antwort | -| MC (mehrere) | `A,C` | Kommagetrennt, ohne Leerzeichen | -| Wahr/Falsch | `A` | `A` = Wahr, `B` = Falsch | -| Freitext | `Berlin` | Das erwartete Wort (Groß/Kleinschreibung egal) | +| Fragetyp | Beispiel-Wert | Erklärung | +|----------------|---------------|------------------------------------------------| +| MC (1 richtig) | `A` | Nur der Buchstabe der richtigen Antwort | +| MC (mehrere) | `A,C` | Kommagetrennt, ohne Leerzeichen | +| Wahr/Falsch | `A` | `A` = Wahr, `B` = Falsch | +| Freitext | `Berlin` | Das erwartete Wort (Groß/Kleinschreibung egal) | ### Freitext-Toleranz @@ -102,39 +102,39 @@ Die App normalisiert Eingaben automatisch: ### MC mit 4 Antworten -| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | -|-------|-----|-----------|-----------|-----------|-----------|-----------------|-----------| -| Wie heißt die Hauptstadt von Baden-Württemberg? | MC | Stuttgart | Karlsruhe | Freiburg | Mannheim | A | Geografie | +| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | +|-------------------------------------------------|-----|-----------|-----------|-----------|-----------|------------------|-----------| +| Wie heißt die Hauptstadt von Baden-Württemberg? | MC | Stuttgart | Karlsruhe | Freiburg | Mannheim | A | Geografie | ### MC mit mehreren richtigen Antworten -| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | -|-------|-----|-----------|-----------|-----------|-----------|-----------------|-----------| -| Welche sind Programmiersprachen? | MC | Python | HTML | Java | Photoshop | A,C | IT | +| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | +|----------------------------------|-----|-----------|-----------|-----------|-----------|------------------|-----------| +| Welche sind Programmiersprachen? | MC | Python | HTML | Java | Photoshop | A,C | IT | ### MC mit 3 Antworten -| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | -|-------|-----|-----------|-----------|-----------|-----------|-----------------|-----------| -| Welche Farbe hat ein Stoppschild? | MC | Rot | Blau | Grün | *(leer)* | A | Verkehr | +| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | +|-----------------------------------|-----|-----------|-----------|-----------|-----------|------------------|-----------| +| Welche Farbe hat ein Stoppschild? | MC | Rot | Blau | Grün | *(leer)* | A | Verkehr | ### Wahr/Falsch -| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | -|-------|-----|-----------|-----------|-----------|-----------|-----------------|-----------| -| Die Erde ist eine Scheibe. | Wahr/Falsch | Wahr | Falsch | *(leer)* | *(leer)* | B | Allgemein | +| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | +|----------------------------|-------------|-----------|-----------|-----------|-----------|------------------|-----------| +| Die Erde ist eine Scheibe. | Wahr/Falsch | Wahr | Falsch | *(leer)* | *(leer)* | B | Allgemein | ### Freitext -| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | -|-------|-----|-----------|-----------|-----------|-----------|-----------------|-----------| -| Wie heißt die Hauptstadt von Frankreich? | Freitext | *(leer)* | *(leer)* | *(leer)* | *(leer)* | Paris | Geografie | +| Frage | Typ | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | +|------------------------------------------|----------|-----------|-----------|-----------|-----------|------------------|-----------| +| Wie heißt die Hauptstadt von Frankreich? | Freitext | *(leer)* | *(leer)* | *(leer)* | *(leer)* | Paris | Geografie | ### Mit Bild -| Frage | Typ | Bild | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | -|-------|-----|------|-----------|-----------|-----------|-----------|-----------------|-----------| -| Was zeigt dieses Verkehrszeichen? | MC | *(Bild hochladen)* | Vorfahrt | Stopp | Einbahnstraße | Parkverbot | B | Verkehr | +| Frage | Typ | Bild | Antwort A | Antwort B | Antwort C | Antwort D | Richtige Antwort | Kategorie | +|-----------------------------------|-----|--------------------|-----------|-----------|---------------|------------|------------------|-----------| +| Was zeigt dieses Verkehrszeichen? | MC | *(Bild hochladen)* | Vorfahrt | Stopp | Einbahnstraße | Parkverbot | B | Verkehr | --- diff --git a/public/js/admin.js b/public/js/admin.js index 4b9205c..3eaf7db 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -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 })); }; diff --git a/public/js/app.js b/public/js/app.js index cf5763e..a84f577 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -13,23 +13,14 @@ const api = { }, }; -function wsUrl() { - return `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}`; -} +function wsUrl() { return `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}`; } function shuffleArray(arr) { const a = [...arr]; - for (let i = a.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [a[i], a[j]] = [a[j], a[i]]; - } + for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1));[a[i], a[j]] = [a[j], a[i]]; } return a; } -/** - * Extract value from Baserow Single Select field. - * Can be { id: 6, value: "MC", color: "..." } or plain string or null. - */ function fieldVal(field, fallback) { if (!field) return fallback || ''; if (typeof field === 'string') return field; @@ -51,7 +42,7 @@ function ImageModal({ src, onClose }) { if (!src) return null; return (
❌ {error}
@@ -298,8 +267,9 @@ function LivePlayPage({ params, navigate }) { if (phase === 'disconnected') return (Verbindung verloren...
- +Verbindung verloren
+Du kannst mit demselben Namen wieder beitreten.
+{session?.setName}
{session?.playerCount || 0} Teilnehmer
@@ -321,33 +292,42 @@ function LivePlayPage({ params, navigate }) { if (phase === 'question') return (✓ Antwort gesendet — warte auf Ergebnis...
}+{result?.yourResult?.points || 0} Punkte
+Gesamt: {result?.yourResult?.totalScore || 0}
+Warte auf nächste Frage...
+{result?.yourResult?.points || 0} Punkte
-Gesamt: {result?.yourResult?.totalScore || 0}
- {result?.correctAnswer && ( -Richtige Antwort: {result.correctAnswer.join(', ')}
- )} -Warte auf nächste Frage...
-{error}
} -Keine Fragensets konfiguriert. Bitte im Admin-Bereich einrichten.
} - {sets.map((s) => ( - - ))} - + + {/* Progress dialog */} + {progressDialog && ( +{progressDialog.setName}: {progressDialog.totalAnswered} / {progressDialog.totalQuestions} Fragen beantwortet
+{progressDialog.totalCorrect} davon richtig
+Keine Fragensets konfiguriert.
} + {sets.map((s) => ( + + ))} + > + )}Richtig: {correctAnswer.join(', ')}
} -{correct}/{total} richtig — {points} Punkte
+{cor}/{total} richtig — {points} Punkte
+ Öffne diesen Link aus dem Admin-Panel.
Die URL muss ?token=PASSWORT#CODE enthalten.
Öffne diesen Link aus dem Admin-Panel.
+ {session?.setName}
Richtig: {result?.correctAnswer?.join(', ')}
-