diff --git a/public/js/admin.js b/public/js/admin.js index 332f06c..1e0ff95 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -60,21 +60,26 @@ function ConfigPage({ config, setConfig, saveConfig }) { const [newSetName, setNewSetName] = useState(''); const [newSetTableId, setNewSetTableId] = useState(''); const [newSetWettkampfCount, setNewSetWettkampfCount] = useState(''); + const [newSetColor, setNewSetColor] = useState('#d97706'); + const [newSetTimer, setNewSetTimer] = useState(''); const [msg, setMsg] = useState(''); const [debugResult, setDebugResult] = useState(null); const addSet = () => { if (!newSetName || !newSetTableId) return; const wkCount = parseInt(newSetWettkampfCount) || null; + const wkTimer = parseInt(newSetTimer) || null; const updated = { ...config, - sets: [...(config.sets || []), { id: Date.now().toString(36), name: newSetName, tableId: parseInt(newSetTableId), wettkampfCount: wkCount }], + sets: [...(config.sets || []), { id: Date.now().toString(36), name: newSetName, tableId: parseInt(newSetTableId), wettkampfCount: wkCount, color: newSetColor, wettkampfTimer: wkTimer }], }; setConfig(updated); saveConfig(updated); setNewSetName(''); setNewSetTableId(''); setNewSetWettkampfCount(''); + setNewSetColor('#d97706'); + setNewSetTimer(''); setMsg('Fragenset hinzugefügt!'); setTimeout(() => setMsg(''), 3000); }; @@ -87,10 +92,20 @@ function ConfigPage({ config, setConfig, saveConfig }) { const updateSetWettkampfCount = (setId, val) => { const wkCount = parseInt(val) || null; - const updated = { - ...config, - sets: config.sets.map((s) => s.id === setId ? { ...s, wettkampfCount: wkCount } : s), - }; + const updated = { ...config, sets: config.sets.map((s) => s.id === setId ? { ...s, wettkampfCount: wkCount } : s) }; + setConfig(updated); + saveConfig(updated); + }; + + const updateSetColor = (setId, val) => { + const updated = { ...config, sets: config.sets.map((s) => s.id === setId ? { ...s, color: val } : s) }; + setConfig(updated); + saveConfig(updated); + }; + + const updateSetTimer = (setId, val) => { + const wkTimer = parseInt(val) || null; + const updated = { ...config, sets: config.sets.map((s) => s.id === setId ? { ...s, wettkampfTimer: wkTimer } : s) }; setConfig(updated); saveConfig(updated); }; @@ -119,21 +134,40 @@ function ConfigPage({ config, setConfig, saveConfig }) { {(config.sets || []).map((s) => (
-
{s.name} (Table ID: {s.tableId})
+
+
+ {s.name} + (Table ID: {s.tableId}) +
-
- ⚔️ Wettkampf-Fragen: - updateSetWettkampfCount(s.id, e.target.value)} /> - {s.wettkampfCount ? ✓ aktiv : nicht konfiguriert} +
+
+ 🎨 + updateSetColor(s.id, e.target.value)} className="w-8 h-8 border rounded cursor-pointer" style={{ padding: 0 }} /> +
+
+ ⚔️ Fragen: + updateSetWettkampfCount(s.id, e.target.value)} /> +
+
+ ⏱ Timer (Min): + updateSetTimer(s.id, e.target.value)} /> +
))} -
- setNewSetName(e.target.value)} /> - setNewSetTableId(e.target.value)} /> - setNewSetWettkampfCount(e.target.value)} /> - +
+
+ setNewSetName(e.target.value)} /> + setNewSetTableId(e.target.value)} /> + setNewSetColor(e.target.value)} className="w-10 h-10 border rounded cursor-pointer" style={{ padding: 0 }} /> +
+
+ setNewSetWettkampfCount(e.target.value)} /> + setNewSetTimer(e.target.value)} /> + +
{msg &&

{msg}

}
diff --git a/public/js/app.js b/public/js/app.js index b8177de..1b0d380 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -365,19 +365,19 @@ function SoloSelectPage({ navigate }) { useEffect(() => { api.get('/api/sets').then(setSets).catch((e) => setError(e.message)); }, []); - const startSet = async (setId, setName2) => { + const startSet = async (setId, setName2, color) => { if (!name.trim()) return setError('Bitte Name eingeben'); setChecking(true); setError(''); try { const progress = await api.get(`/api/progress/${encodeURIComponent(name.trim())}/${setId}`); if (progress.hasProgress) { - setProgressDialog({ setId, setName: setName2, ...progress }); + setProgressDialog({ setId, setName: setName2, color, ...progress }); } else { - navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle }); + navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle, color }); } } catch (e) { - navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle }); + navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle, color }); } setChecking(false); }; @@ -385,7 +385,7 @@ function SoloSelectPage({ navigate }) { const continueSession = () => { navigate('solo-quiz', { setId: progressDialog.setId, setName: progressDialog.setName, - name: name.trim(), shuffle: doShuffle, + name: name.trim(), shuffle: doShuffle, color: progressDialog.color, continueSessionId: progressDialog.sessionId, priorAnswers: progressDialog.answers, }); @@ -395,7 +395,7 @@ function SoloSelectPage({ navigate }) { const startFresh = () => { navigate('solo-quiz', { setId: progressDialog.setId, setName: progressDialog.setName, - name: name.trim(), shuffle: doShuffle, + name: name.trim(), shuffle: doShuffle, color: progressDialog.color, }); setProgressDialog(null); }; @@ -428,12 +428,16 @@ function SoloSelectPage({ navigate }) { <>

Fragenset wählen:

{sets.length === 0 &&

Keine Fragensets konfiguriert.

} - {sets.map((s) => ( - - ))} + {sets.map((s) => { + const bg = s.color || '#d97706'; + return ( + + ); + })} )}
@@ -487,7 +491,7 @@ function SoloQuizPage({ params, navigate }) {

✅ Alle Fragen bereits beantwortet!

- @@ -584,7 +588,7 @@ function SoloQuizPage({ params, navigate }) { ...priorResults.map((a) => ({ Frage_ID: a.Frage_ID, Richtig: a.Richtig, Punkte: a.Punkte })), ...results, ]; - navigate('solo-result', { results: allResults, setName: params.setName, name: params.name, questions: allQuestions }); + navigate('solo-result', { results: allResults, setName: params.setName, name: params.name, questions: allQuestions, color: params.color }); return; } setIndex(index + 1); @@ -601,13 +605,15 @@ function SoloQuizPage({ params, navigate }) { Frage {totalProgress} / {totalAll}
-
+

{sanitized.frage}

{sanitized.bild && setEnlargedImg(sanitized.bild)} />} {!showFeedback && ( - + )} {showFeedback && (
@@ -665,7 +671,9 @@ function SoloResultPage({ params, navigate }) {
)}
- +
@@ -712,10 +720,10 @@ function WettkampfSelectPage({ navigate }) { if (progress.hasProgress) { setProgressDialog({ ...progress, count }); } else { - navigate('wettkampf-quiz', { setId: selectedSet.id, setName: selectedSet.name, name: name.trim(), questionCount: count }); + navigate('wettkampf-quiz', { setId: selectedSet.id, setName: selectedSet.name, name: name.trim(), questionCount: count, color: selectedSet.color, timer: selectedSet.wettkampfTimer }); } } catch (e) { - navigate('wettkampf-quiz', { setId: selectedSet.id, setName: selectedSet.name, name: name.trim(), questionCount: count }); + navigate('wettkampf-quiz', { setId: selectedSet.id, setName: selectedSet.name, name: name.trim(), questionCount: count, color: selectedSet.color, timer: selectedSet.wettkampfTimer }); } setChecking(false); }; @@ -724,6 +732,7 @@ function WettkampfSelectPage({ navigate }) { navigate('wettkampf-quiz', { setId: selectedSet.id, setName: selectedSet.name, name: name.trim(), questionCount: selectedSet.wettkampfCount, + color: selectedSet.color, timer: selectedSet.wettkampfTimer, continueSessionId: progressDialog.sessionId, priorAnswers: progressDialog.answers, }); @@ -734,6 +743,7 @@ function WettkampfSelectPage({ navigate }) { navigate('wettkampf-quiz', { setId: selectedSet.id, setName: selectedSet.name, name: name.trim(), questionCount: selectedSet.wettkampfCount, + color: selectedSet.color, timer: selectedSet.wettkampfTimer, }); setProgressDialog(null); }; @@ -762,12 +772,16 @@ function WettkampfSelectPage({ navigate }) { <>

Fragenset wählen:

{sets.length === 0 &&

Keine Wettkampf-Sets konfiguriert. Bitte im Admin-Bereich konfigurieren.

} - {sets.map((s) => ( - - ))} + {sets.map((s) => { + const bg = s.color || '#059669'; + return ( + + ); + })} ) : ( <> @@ -783,7 +797,8 @@ function WettkampfSelectPage({ navigate }) { {error &&

{error}

} @@ -809,7 +824,12 @@ function WettkampfQuizPage({ params, navigate }) { const [shuffledAnswersMap, setShuffledAnswersMap] = useState({}); const [saving, setSaving] = useState(false); const [confirming, setConfirming] = useState(false); + const [timeLeft, setTimeLeft] = useState(null); // seconds remaining + const [timerExpired, setTimerExpired] = useState(false); const sessionId = useRef(params.continueSessionId || 'WK-' + Date.now().toString(36).toUpperCase()); + const timerRef = useRef(null); + const confirmRef = useRef(null); + const accentColor = params.color || '#059669'; // Type-aware random selection const selectQuestions = useCallback((allQuestions, count) => { @@ -991,7 +1011,11 @@ function WettkampfQuizPage({ params, navigate }) { const finalResults = []; questions.forEach((question) => { const a = answers[question.id]; - if (!a) return; + if (!a || !a.selected || a.selected.length === 0 || a.selected[0] === '') { + // Unanswered — counts as wrong + finalResults.push({ rowId: null, questionId: question.id, richtig: false, punkte: 0 }); + return; + } const typ = fieldVal(question['Typ'], 'MC').toLowerCase(); const rawCorrect = (question['Richtige Antwort'] || '').trim(); @@ -1057,9 +1081,33 @@ function WettkampfQuizPage({ params, navigate }) { name: params.name, allQuestionsRaw: allQuestionsRaw, shuffledAnswersMap: shuffledAnswersMap, + color: params.color, }); }; + // Store confirmResults in ref for timer access + confirmRef.current = confirmResults; + + // Countdown timer + useEffect(() => { + if (!params.timer || questions.length === 0) return; + const totalSeconds = params.timer * 60; + setTimeLeft(totalSeconds); + const start = Date.now(); + timerRef.current = setInterval(() => { + const elapsed = (Date.now() - start) / 1000; + const remaining = Math.max(0, totalSeconds - elapsed); + setTimeLeft(remaining); + if (remaining <= 0) { + clearInterval(timerRef.current); + setTimerExpired(true); + // Auto-confirm + if (confirmRef.current) confirmRef.current(); + } + }, 200); + return () => clearInterval(timerRef.current); + }, [questions.length, params.timer]); + if (error) return
{error}
; if (!questions.length) return
Lade Fragen...
; @@ -1086,9 +1134,22 @@ function WettkampfQuizPage({ params, navigate }) { Frage {index + 1} / {questions.length}
+ {/* Timer */} + {timeLeft !== null && ( +
+
+
= params.timer * 30 ? accentColor : undefined }} /> +
+
+ {Math.floor(timeLeft / 60)}:{String(Math.floor(timeLeft % 60)).padStart(2, '0')} +
+
+ )} + {/* Progress bar */}
-
+
{/* Question navigator */} @@ -1097,17 +1158,18 @@ function WettkampfQuizPage({ params, navigate }) { const a = answers[question.id]; const isAnswered = a && a.selected && a.selected.length > 0 && a.selected[0] !== ''; const isCurrent = i === index; + const style = {}; let cls = 'w-9 h-9 rounded-lg text-sm font-bold transition-all '; - if (isCurrent) cls += 'bg-amber-500 text-white ring-2 ring-amber-300'; - else if (isAnswered) cls += 'bg-emerald-600 text-white'; + if (isCurrent) { cls += 'text-white ring-2'; style.backgroundColor = '#f59e0b'; style.boxShadow = '0 0 0 2px #fcd34d'; } + else if (isAnswered) { cls += 'text-white'; style.backgroundColor = accentColor; } else cls += 'bg-gray-700 text-gray-400 hover:bg-gray-600'; - return ; + return ; })}
{/* Status */}
- {answeredCount} + {answeredCount} / {questions.length} beantwortet
@@ -1141,8 +1203,9 @@ function WettkampfQuizPage({ params, navigate }) { {/* Action buttons */}
{hasCurrentAnswer && ( - )} @@ -1304,7 +1367,9 @@ function WettkampfResultPage({ params, navigate }) { )}
- +
diff --git a/server/index.js b/server/index.js index 434f1a5..ce81a20 100644 --- a/server/index.js +++ b/server/index.js @@ -90,7 +90,7 @@ function rewriteImageUrls(rows) { app.get('/api/sets', (req, res) => { const config = readConfig(); - res.json((config.sets || []).map((s) => ({ id: s.id, name: s.name, wettkampfCount: s.wettkampfCount || null }))); + res.json((config.sets || []).map((s) => ({ id: s.id, name: s.name, wettkampfCount: s.wettkampfCount || null, color: s.color || null, wettkampfTimer: s.wettkampfTimer || null }))); }); app.get('/api/sets/:id/questions', async (req, res) => {