-

+
🚨
Quizalarm Admin
setPw(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && submit()} autoFocus />
{err &&
{err}
}
@@ -250,7 +250,14 @@ function LiveSetupPage({ config, token, onSessionCreated }) {
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'session-created') {
- onSessionCreated(msg.code, ws);
+ onSessionCreated(msg.code, ws, {
+ code: msg.code,
+ setName: msg.setName,
+ questionCount: msg.questionCount,
+ options: msg.options,
+ players: [],
+ playerCount: 0,
+ });
} else if (msg.type === 'error') {
setError(msg.message);
setLoading(false);
@@ -298,11 +305,11 @@ function LiveSetupPage({ config, token, onSessionCreated }) {
}
/* ================================================================== */
-/* Live Control Page */
+/* Live Control Page (FIX: sends admin-join on mount) */
/* ================================================================== */
-function LiveControlPage({ token, code, ws }) {
- const [session, setSession] = useState(null);
+function LiveControlPage({ token, code, ws, initialSession }) {
+ const [session, setSession] = useState(initialSession || null);
const [phase, setPhase] = useState('waiting');
const [answerCount, setAnswerCount] = useState(0);
const [result, setResult] = useState(null);
@@ -313,20 +320,26 @@ function LiveControlPage({ token, code, ws }) {
const handler = (e) => {
const msg = JSON.parse(e.data);
+ console.log('[admin-control] Received:', msg.type);
switch (msg.type) {
case 'session-created':
- setSession({ code: msg.code, setName: msg.setName, questionCount: msg.questionCount, options: msg.options, players: [] });
+ setSession({ code: msg.code, setName: msg.setName, questionCount: msg.questionCount, options: msg.options, players: [], playerCount: 0 });
+ setPhase('waiting');
break;
case 'admin-joined':
setSession(msg.session);
+ setLeaderboardData(msg.session.leaderboard || []);
if (msg.session.status === 'waiting') setPhase('waiting');
else if (msg.session.status === 'ended') setPhase('ended');
- else setPhase('active');
- setLeaderboardData(msg.session.leaderboard);
+ else if (msg.session.status === 'showing-result') setPhase('result');
+ else setPhase('question');
break;
case 'player-joined':
case 'player-left':
- setSession((s) => s ? { ...s, players: msg.players, playerCount: msg.playerCount } : s);
+ setSession((s) => {
+ if (!s) return { code, players: msg.players, playerCount: msg.playerCount };
+ return { ...s, players: msg.players, playerCount: msg.playerCount };
+ });
break;
case 'game-start':
setPhase('active');
@@ -355,42 +368,67 @@ function LiveControlPage({ token, code, ws }) {
};
ws.addEventListener('message', handler);
- return () => ws.removeEventListener('message', handler);
- }, [ws]);
- const start = () => ws.send(JSON.stringify({ type: 'admin-start', token, code }));
- const showResultAction = () => ws.send(JSON.stringify({ type: 'admin-show-result', token, code }));
- const next = () => ws.send(JSON.stringify({ type: 'admin-next', token, code }));
- const end = () => ws.send(JSON.stringify({ type: 'admin-end', 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 = () => {
+ ws.send(JSON.stringify({ type: 'admin-join', token, code }));
+ ws.removeEventListener('open', onOpen);
+ };
+ ws.addEventListener('open', onOpen);
+ }
+
+ return () => ws.removeEventListener('message', handler);
+ }, [ws, code, token]);
+
+ const start = () => {
+ console.log('[admin-control] Sending admin-start');
+ 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 }));
+ };
const presentUrl = `${location.origin}/present?token=${encodeURIComponent(token)}#${code}`;
+ const playerCount = session?.players?.length || session?.playerCount || 0;
return (
Session-Code
{code}
-
{session?.setName} — {session?.questionCount || '?'} Fragen
+
{session?.setName || '...'} — {session?.questionCount || '?'} Fragen
📺 Präsentations-Fenster öffnen
-
Teilnehmer ({session?.players?.length || 0})
+
Teilnehmer ({playerCount})
{(session?.players || []).map((n) => {n})}
- {(!session?.players || session.players.length === 0) &&
Noch keine Teilnehmer...
}
+ {playerCount === 0 &&
Noch keine Teilnehmer...
}
{phase === 'waiting' && (
-
diff --git a/public/js/app.js b/public/js/app.js
index 9969948..cf5763e 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -26,6 +26,23 @@ function shuffleArray(arr) {
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;
+ if (typeof field === 'object' && field.value !== undefined) return field.value;
+ return String(field);
+}
+
+function normalizeText(t) {
+ return (t || '').toLowerCase()
+ .replace(/[äÄ]/g, 'ae').replace(/[öÖ]/g, 'oe').replace(/[üÜ]/g, 'ue').replace(/ß/g, 'ss')
+ .replace(/[^a-z0-9]/g, '');
+}
+
/* ================================================================== */
/* Shared Components */
/* ================================================================== */
@@ -123,7 +140,7 @@ function AnswerGrid({ question, selected, onToggle, disabled }) {
function HomePage({ navigate }) {
return (
-

+
🚨
Quizalarm
Lernen. Quizzen. Wissen.
@@ -402,6 +419,7 @@ function SoloQuizPage({ params, navigate }) {
useEffect(() => {
api.get(`/api/sets/${params.setId}/questions`).then((qs) => {
+ console.log(`Loaded ${qs.length} questions, sample Typ:`, qs[0]?.['Typ']);
if (params.shuffle) {
for (let i = qs.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@@ -412,7 +430,6 @@ function SoloQuizPage({ params, navigate }) {
}).catch((e) => setError(e.message));
}, []);
- // Shuffle answers when question changes
useEffect(() => {
if (questions.length === 0 || index >= questions.length) return;
const q = questions[index];
@@ -427,16 +444,14 @@ function SoloQuizPage({ params, navigate }) {
if (!questions.length) return
Lade Fragen...
;
const q = questions[index];
- const typ = (q['Typ'] || 'MC').toLowerCase();
+ const typ = fieldVal(q['Typ'], 'MC').toLowerCase();
const sanitized = {
- frage: q['Frage'] || q['frage'] || '(Kein Fragetext)',
- typ: q['Typ'] || q['typ'] || 'MC',
+ frage: q['Frage'] || '(Kein Fragetext)',
+ typ: fieldVal(q['Typ'], 'MC'),
bild: q['Bild'] && Array.isArray(q['Bild']) && q['Bild'].length ? q['Bild'][0].url : null,
antworten: shuffledAnswers,
};
- const normalizeText = (t) => (t || '').toLowerCase().replace(/[äÄ]/g, 'ae').replace(/[öÖ]/g, 'oe').replace(/[üÜ]/g, 'ue').replace(/ß/g, 'ss').replace(/[^a-z0-9]/g, '');
-
const checkAnswer = () => {
const rawCorrect = (q['Richtige Antwort'] || '').trim();
let correct, ok;
@@ -520,11 +535,10 @@ function SoloResultPage({ params, navigate }) {
const byCategory = {};
results.forEach((r, i) => {
const q = questions[i];
- const cat = (q && (q['Kategorie'] || q['kategorie'])) || 'Ohne Kategorie';
- const catName = typeof cat === 'object' && cat.value ? cat.value : cat;
- if (!byCategory[catName]) byCategory[catName] = { total: 0, correct: 0 };
- byCategory[catName].total++;
- if (r.Richtig) byCategory[catName].correct++;
+ const cat = fieldVal(q?.['Kategorie'], 'Ohne Kategorie') || 'Ohne Kategorie';
+ if (!byCategory[cat]) byCategory[cat] = { total: 0, correct: 0 };
+ byCategory[cat].total++;
+ if (r.Richtig) byCategory[cat].correct++;
});
return (
@@ -573,7 +587,7 @@ function MyResultsPage({ navigate }) {
if (results) {
results.forEach((r) => {
const sid = r['Session_ID'] || 'Unbekannt';
- if (!sessions[sid]) sessions[sid] = { modus: r['Modus'], fragenset: r['Fragenset'], date: r['Zeitstempel'], answers: [] };
+ if (!sessions[sid]) sessions[sid] = { modus: fieldVal(r['Modus'], ''), fragenset: r['Fragenset'], date: r['Zeitstempel'], answers: [] };
sessions[sid].answers.push(r);
});
}
diff --git a/server/quiz-logic.js b/server/quiz-logic.js
index 34a54b1..bc0b0f4 100644
--- a/server/quiz-logic.js
+++ b/server/quiz-logic.js
@@ -14,13 +14,28 @@ function normalizeText(text) {
.trim();
}
+/**
+ * Extract the string value from a Baserow Single Select field.
+ * Baserow returns { id: 6, value: "MC", color: "..." } for Single Select.
+ */
+function extractFieldValue(field, fallback) {
+ if (!field) return fallback || '';
+ if (typeof field === 'string') return field;
+ if (typeof field === 'object' && field.value !== undefined) return field.value;
+ return String(field);
+}
+
+function getTyp(question) {
+ return extractFieldValue(question['Typ'], 'MC').toLowerCase();
+}
+
function checkFreitext(userAnswer, correctAnswer) {
return normalizeText(userAnswer) === normalizeText(correctAnswer);
}
function parseCorrectAnswers(question) {
const raw = (question['Richtige Antwort'] || '').trim();
- const typ = (question['Typ'] || 'MC').toLowerCase();
+ const typ = getTyp(question);
if (typ === 'freitext') return [raw];
return raw
.split(',')
@@ -60,7 +75,7 @@ function calculateScore(question, selected, timeRemaining, totalTime, scoringMod
basis = 1000;
}
- const typ = (question['Typ'] || 'MC').toLowerCase();
+ const typ = getTyp(question);
const correctAnswers = parseCorrectAnswers(question);
if (typ === 'freitext') {
@@ -93,14 +108,14 @@ function calculateScore(question, selected, timeRemaining, totalTime, scoringMod
* Strip the correct answer and shuffle options before sending to participants.
*/
function sanitizeQuestion(question) {
- const typ = (question['Typ'] || 'MC').toLowerCase();
+ const typ = getTyp(question);
const sanitized = {
id: question.id,
frage: question['Frage'] || '',
- typ: question['Typ'] || 'MC',
+ typ: extractFieldValue(question['Typ'], 'MC'),
bild: null,
antworten: [],
- kategorie: question['Kategorie'] || '',
+ kategorie: extractFieldValue(question['Kategorie'], ''),
};
const bild = question['Bild'];
@@ -114,11 +129,10 @@ function sanitizeQuestion(question) {
const val = question[`Antwort ${k}`];
if (val) sanitized.antworten.push({ key: k, text: val });
}
- // Shuffle answer order
sanitized.antworten = shuffleArray(sanitized.antworten);
}
return sanitized;
}
-module.exports = { calculateScore, sanitizeQuestion, parseCorrectAnswers, normalizeText, shuffleArray };
\ No newline at end of file
+module.exports = { calculateScore, sanitizeQuestion, parseCorrectAnswers, normalizeText, shuffleArray, extractFieldValue };
\ No newline at end of file