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}
+
))}
-
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!
-
{sanitized.frage}
{sanitized.bild &&

setEnlargedImg(sanitized.bild)} />}
{!showFeedback && (
-
Prüfen
+
Prüfen
)}
{showFeedback && (
@@ -665,7 +671,9 @@ function SoloResultPage({ params, navigate }) {
)}
- navigate('solo-select')} className="w-full py-3 bg-amber-600 hover:bg-amber-700 rounded-xl font-bold shadow-lg shadow-amber-900/30">Nochmal spielen
+ navigate('solo-select')}
+ style={{ backgroundColor: params.color || '#d97706' }}
+ className="w-full py-3 hover:opacity-90 rounded-xl font-bold shadow-lg text-white">Nochmal spielen
navigate('home')} className="w-full py-3 bg-gray-700 hover:bg-gray-600 rounded-xl font-bold">Zur Startseite
@@ -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) => (
- selectSet(s)}
- className="w-full py-4 bg-emerald-600 hover:bg-emerald-700 rounded-xl text-lg font-bold transition-all shadow-lg shadow-emerald-900/30">
- {s.name} ({s.wettkampfCount} Fragen)
-
- ))}
+ {sets.map((s) => {
+ const bg = s.color || '#059669';
+ return (
+ selectSet(s)}
+ style={{ backgroundColor: bg }}
+ className="w-full py-4 rounded-xl text-lg font-bold transition-all shadow-lg text-white hover:opacity-90">
+ {s.name} ({s.wettkampfCount} Fragen)
+
+ );
+ })}
>
) : (
<>
@@ -783,7 +797,8 @@ function WettkampfSelectPage({ navigate }) {
{error && {error}
}
+ style={{ backgroundColor: selectedSet.color || '#059669' }}
+ className="w-full py-4 hover:opacity-90 disabled:opacity-40 rounded-xl text-xl font-bold transition-all shadow-lg text-white">
{checking ? 'Prüfe...' : '⚔️ Wettkampf starten'}
>
@@ -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
setIndex(i)} className={cls}>{i + 1};
+ return
setIndex(i)} className={cls} style={style}>{i + 1};
})}
{/* Status */}
- {answeredCount}
+ {answeredCount}
/ {questions.length} beantwortet
@@ -1141,8 +1203,9 @@ function WettkampfQuizPage({ params, navigate }) {
{/* Action buttons */}
{hasCurrentAnswer && (
-
+
{saving ? 'Speichere...' : 'Antwort speichern →'}
)}
@@ -1304,7 +1367,9 @@ function WettkampfResultPage({ params, navigate }) {
)}
- navigate('wettkampf-select')} className="w-full py-3 bg-emerald-600 hover:bg-emerald-700 rounded-xl font-bold shadow-lg shadow-emerald-900/30">Nochmal spielen
+ navigate('wettkampf-select')}
+ style={{ backgroundColor: params.color || '#059669' }}
+ className="w-full py-3 hover:opacity-90 rounded-xl font-bold shadow-lg text-white">Nochmal spielen
navigate('home')} className="w-full py-3 bg-gray-700 hover:bg-gray-600 rounded-xl font-bold">Zur Startseite
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) => {