UI overhaul: fire-gradient theme and visual polish; logic: added answer shuffling and improved Baserow error handling
This commit is contained in:
+81
-61
@@ -1,8 +1,5 @@
|
||||
const { useState, useEffect, useRef, useCallback } = React;
|
||||
|
||||
/* ================================================================== */
|
||||
/* API Helper */
|
||||
/* ================================================================== */
|
||||
const api = {
|
||||
async get(url) {
|
||||
const r = await fetch(url);
|
||||
@@ -20,6 +17,15 @@ 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]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Shared Components */
|
||||
/* ================================================================== */
|
||||
@@ -27,8 +33,8 @@ function wsUrl() {
|
||||
function ImageModal({ src, onClose }) {
|
||||
if (!src) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 p-4" onClick={onClose}>
|
||||
<img src={src} className="max-w-full max-h-full object-contain rounded-lg" alt="Vergrößerung" />
|
||||
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4" onClick={onClose}>
|
||||
<img src={src} className="max-w-full max-h-full object-contain rounded-lg" alt="Vergroesserung" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -38,9 +44,11 @@ function Timer({ timeLeft, total }) {
|
||||
const pct = Math.max(0, (timeLeft / total) * 100);
|
||||
const color = pct > 50 ? 'bg-green-500' : pct > 25 ? 'bg-yellow-500' : 'bg-red-500';
|
||||
return (
|
||||
<div className="w-full bg-gray-700 rounded-full h-4 mb-4 overflow-hidden">
|
||||
<div className={`${color} h-full rounded-full transition-all duration-200`} style={{ width: `${pct}%` }} />
|
||||
<div className="text-center text-sm mt-1">{Math.ceil(timeLeft)}s</div>
|
||||
<div className="w-full mb-4">
|
||||
<div className="w-full bg-gray-700 rounded-full h-4 overflow-hidden">
|
||||
<div className={`${color} h-full rounded-full transition-all duration-200`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="text-center text-sm mt-1 text-gray-300">{Math.ceil(timeLeft)}s</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,11 +57,11 @@ function Leaderboard({ entries, highlight, compact }) {
|
||||
if (!entries || !entries.length) return null;
|
||||
const show = compact ? entries.slice(0, 5) : entries;
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-xl p-4 mt-4">
|
||||
<div className="bg-gray-800 rounded-xl p-4 mt-4 w-full">
|
||||
<h3 className="text-lg font-bold mb-2 text-center">🏆 Leaderboard</h3>
|
||||
{show.map((e) => (
|
||||
<div key={e.name} className={`flex justify-between py-2 px-3 rounded-lg mb-1 ${e.name === highlight ? 'bg-indigo-600' : 'bg-gray-700'}`}>
|
||||
<span className="font-medium">{e.rank}. {e.name}</span>
|
||||
{show.map((e, i) => (
|
||||
<div key={e.name} className={`flex justify-between py-2 px-3 rounded-lg mb-1 ${e.name === highlight ? 'bg-red-700' : i === 0 ? 'bg-amber-700' : 'bg-gray-700'}`}>
|
||||
<span className="font-medium">{i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${e.rank}.`} {e.name}</span>
|
||||
<span className="font-bold">{e.score}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -71,7 +79,7 @@ function AnswerGrid({ question, selected, onToggle, disabled }) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
className="w-full p-4 rounded-xl bg-gray-700 text-white text-xl text-center border-2 border-gray-600 focus:border-indigo-500 outline-none"
|
||||
className="w-full p-4 rounded-xl bg-gray-700 text-white text-xl text-center border-2 border-gray-600 focus:border-red-500 outline-none"
|
||||
placeholder="Deine Antwort..."
|
||||
value={selected[0] || ''}
|
||||
onChange={(e) => onToggle([e.target.value])}
|
||||
@@ -100,7 +108,7 @@ function AnswerGrid({ question, selected, onToggle, disabled }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="opacity-70 mr-2">{a.key}</span> {a.text}
|
||||
{a.text}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -115,11 +123,12 @@ function AnswerGrid({ question, selected, onToggle, disabled }) {
|
||||
function HomePage({ navigate }) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<h1 className="text-5xl font-extrabold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 to-pink-500">⚡ Quizalarm</h1>
|
||||
<div className="text-6xl mb-2">🚨</div>
|
||||
<h1 className="text-5xl font-extrabold mb-2 fire-gradient">Quizalarm</h1>
|
||||
<p className="text-gray-400 mb-10 text-lg">Lernen. Quizzen. Wissen.</p>
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
<button onClick={() => navigate('join')} className="w-full py-4 bg-indigo-600 hover:bg-indigo-700 rounded-xl text-xl font-bold transition-all">🎮 Live-Quiz beitreten</button>
|
||||
<button onClick={() => navigate('solo-select')} className="w-full py-4 bg-emerald-600 hover:bg-emerald-700 rounded-xl text-xl font-bold transition-all">📚 Solo-Lernen</button>
|
||||
<button onClick={() => navigate('join')} className="w-full py-4 bg-red-600 hover:bg-red-700 rounded-xl text-xl font-bold transition-all shadow-lg shadow-red-900/30">🎮 Live-Quiz beitreten</button>
|
||||
<button onClick={() => navigate('solo-select')} className="w-full py-4 bg-amber-600 hover:bg-amber-700 rounded-xl text-xl font-bold transition-all shadow-lg shadow-amber-900/30">📚 Solo-Lernen</button>
|
||||
<button onClick={() => navigate('my-results')} className="w-full py-4 bg-gray-700 hover:bg-gray-600 rounded-xl text-xl font-bold transition-all">📊 Meine Ergebnisse</button>
|
||||
</div>
|
||||
<a href="/admin" className="mt-8 text-gray-500 hover:text-gray-300 text-sm">Admin-Bereich →</a>
|
||||
@@ -145,10 +154,10 @@ function JoinPage({ navigate }) {
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
<h2 className="text-3xl font-bold mb-6">🎮 Live-Quiz beitreten</h2>
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-2xl tracking-widest border-2 border-gray-700 focus:border-indigo-500 outline-none uppercase" placeholder="CODE" value={code} onChange={(e) => setCode(e.target.value)} maxLength={6} />
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-xl border-2 border-gray-700 focus:border-indigo-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} maxLength={20} onKeyDown={(e) => e.key === 'Enter' && handleJoin()} />
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-2xl tracking-widest border-2 border-gray-700 focus:border-red-500 outline-none uppercase" placeholder="CODE" value={code} onChange={(e) => setCode(e.target.value)} maxLength={6} />
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-xl border-2 border-gray-700 focus:border-red-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} maxLength={20} onKeyDown={(e) => e.key === 'Enter' && handleJoin()} />
|
||||
{error && <p className="text-red-400 text-center">{error}</p>}
|
||||
<button onClick={handleJoin} className="w-full py-4 bg-indigo-600 hover:bg-indigo-700 rounded-xl text-xl font-bold">Beitreten</button>
|
||||
<button onClick={handleJoin} className="w-full py-4 bg-red-600 hover:bg-red-700 rounded-xl text-xl font-bold shadow-lg shadow-red-900/30">Beitreten</button>
|
||||
<button onClick={() => navigate('home')} className="w-full py-3 text-gray-400 hover:text-white">← Zurück</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,6 +184,8 @@ function LivePlayPage({ params, navigate }) {
|
||||
const [enlargedImg, setEnlargedImg] = useState(null);
|
||||
const wsRef = useRef(null);
|
||||
const timerRef = useRef(null);
|
||||
const phaseRef = useRef(phase);
|
||||
phaseRef.current = phase;
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket(wsUrl());
|
||||
@@ -236,13 +247,12 @@ function LivePlayPage({ params, navigate }) {
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (phase !== 'final' && phase !== 'error') setPhase('disconnected');
|
||||
if (phaseRef.current !== 'final' && phaseRef.current !== 'error') setPhase('disconnected');
|
||||
};
|
||||
|
||||
return () => { clearInterval(timerRef.current); ws.close(); };
|
||||
}, []);
|
||||
|
||||
// Timer countdown
|
||||
useEffect(() => {
|
||||
clearInterval(timerRef.current);
|
||||
if (phase === 'question' && timeLimit > 0 && !answered) {
|
||||
@@ -276,7 +286,7 @@ function LivePlayPage({ params, navigate }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (phase === 'connecting') return <div className="min-h-screen flex items-center justify-center"><p className="text-xl animate-pulse">Verbinde...</p></div>;
|
||||
if (phase === 'connecting') return <div className="min-h-screen flex items-center justify-center"><p className="text-xl animate-pulse text-amber-400">Verbinde...</p></div>;
|
||||
|
||||
if (phase === 'waiting') return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-6">
|
||||
@@ -285,7 +295,7 @@ function LivePlayPage({ params, navigate }) {
|
||||
<p className="text-lg mb-4">{session?.playerCount || 0} Teilnehmer</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center max-w-md">
|
||||
{(session?.players || []).map((n) => (
|
||||
<span key={n} className={`px-3 py-1 rounded-full text-sm ${n === params.name ? 'bg-indigo-600' : 'bg-gray-700'}`}>{n}</span>
|
||||
<span key={n} className={`px-3 py-1 rounded-full text-sm ${n === params.name ? 'bg-red-600' : 'bg-gray-700'}`}>{n}</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-6 text-gray-500 animate-pulse">Warte auf den Start...</p>
|
||||
@@ -300,9 +310,9 @@ function LivePlayPage({ params, navigate }) {
|
||||
{question?.bild && <img src={question.bild} className="max-h-48 mx-auto rounded-lg mb-4 cursor-pointer" alt="" onClick={() => setEnlargedImg(question.bild)} />}
|
||||
<AnswerGrid question={question} selected={selected} onToggle={setSelected} disabled={answered} />
|
||||
{!answered && (
|
||||
<button onClick={submitAnswer} disabled={selected.length === 0} className="mt-4 py-3 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 rounded-xl text-lg font-bold w-full">Antwort senden</button>
|
||||
<button onClick={submitAnswer} disabled={selected.length === 0} className="mt-4 py-3 bg-red-600 hover:bg-red-700 disabled:opacity-40 rounded-xl text-lg font-bold w-full shadow-lg shadow-red-900/30">Antwort senden</button>
|
||||
)}
|
||||
{answered && <p className="mt-4 text-center text-green-400 animate-pulse">✓ Antwort gesendet — warte auf Ergebnis...</p>}
|
||||
{answered && <p className="mt-4 text-center text-amber-400 animate-pulse">✓ Antwort gesendet — warte auf Ergebnis...</p>}
|
||||
<ImageModal src={enlargedImg} onClose={() => setEnlargedImg(null)} />
|
||||
</div>
|
||||
);
|
||||
@@ -315,7 +325,7 @@ function LivePlayPage({ params, navigate }) {
|
||||
<p className="text-2xl font-bold mb-1">+{result?.yourResult?.points || 0} Punkte</p>
|
||||
<p className="text-gray-400 mb-4">Gesamt: {result?.yourResult?.totalScore || 0}</p>
|
||||
{result?.correctAnswer && (
|
||||
<p className="bg-gray-800 py-2 px-4 rounded-lg mb-4">Richtige Antwort: <strong>{result.correctAnswer.join(', ')}</strong></p>
|
||||
<p className="bg-gray-800 py-2 px-4 rounded-lg mb-4">Richtige Antwort: <strong className="text-amber-400">{result.correctAnswer.join(', ')}</strong></p>
|
||||
)}
|
||||
<Leaderboard entries={leaderboardData} highlight={params.name} compact />
|
||||
<p className="mt-4 text-gray-500 animate-pulse">Warte auf nächste Frage...</p>
|
||||
@@ -340,7 +350,7 @@ function LivePlayPage({ params, navigate }) {
|
||||
function SoloSelectPage({ navigate }) {
|
||||
const [sets, setSets] = useState([]);
|
||||
const [name, setName] = useState('');
|
||||
const [shuffle, setShuffle] = useState(true);
|
||||
const [doShuffle, setDoShuffle] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -349,23 +359,23 @@ function SoloSelectPage({ navigate }) {
|
||||
|
||||
const start = (setId, setName2) => {
|
||||
if (!name.trim()) return setError('Bitte Name eingeben');
|
||||
navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle });
|
||||
navigate('solo-quiz', { setId, setName: setName2, name: name.trim(), shuffle: doShuffle });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center p-6 pt-12">
|
||||
<h2 className="text-3xl font-bold mb-6">📚 Solo-Lernen</h2>
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-xl border-2 border-gray-700 focus:border-emerald-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center text-xl border-2 border-gray-700 focus:border-amber-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<label className="flex items-center gap-3 text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" checked={shuffle} onChange={() => setShuffle(!shuffle)} className="w-5 h-5 accent-emerald-500" />
|
||||
<input type="checkbox" checked={doShuffle} onChange={() => setDoShuffle(!doShuffle)} className="w-5 h-5 accent-amber-500" />
|
||||
Fragen mischen
|
||||
</label>
|
||||
{error && <p className="text-red-400">{error}</p>}
|
||||
<h3 className="text-lg font-semibold mt-4">Fragenset wählen:</h3>
|
||||
{sets.length === 0 && <p className="text-gray-500">Keine Fragensets konfiguriert.</p>}
|
||||
{sets.length === 0 && <p className="text-gray-500">Keine Fragensets konfiguriert. Bitte im Admin-Bereich einrichten.</p>}
|
||||
{sets.map((s) => (
|
||||
<button key={s.id} onClick={() => start(s.id, s.name)} className="w-full py-4 bg-emerald-700 hover:bg-emerald-600 rounded-xl text-lg font-bold transition-all">{s.name}</button>
|
||||
<button key={s.id} onClick={() => start(s.id, s.name)} className="w-full py-4 bg-amber-600 hover:bg-amber-700 rounded-xl text-lg font-bold transition-all shadow-lg shadow-amber-900/30">{s.name}</button>
|
||||
))}
|
||||
<button onClick={() => navigate('home')} className="w-full py-3 text-gray-400 hover:text-white">← Zurück</button>
|
||||
</div>
|
||||
@@ -387,6 +397,7 @@ function SoloQuizPage({ params, navigate }) {
|
||||
const [results, setResults] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const [enlargedImg, setEnlargedImg] = useState(null);
|
||||
const [shuffledAnswers, setShuffledAnswers] = useState([]);
|
||||
const sessionId = useRef('SOLO-' + Date.now().toString(36).toUpperCase());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -401,27 +412,37 @@ function SoloQuizPage({ params, navigate }) {
|
||||
}).catch((e) => setError(e.message));
|
||||
}, []);
|
||||
|
||||
if (error) return <div className="min-h-screen flex items-center justify-center text-red-400 text-xl">{error}</div>;
|
||||
if (!questions.length) return <div className="min-h-screen flex items-center justify-center animate-pulse text-xl">Lade Fragen...</div>;
|
||||
// Shuffle answers when question changes
|
||||
useEffect(() => {
|
||||
if (questions.length === 0 || index >= questions.length) return;
|
||||
const q = questions[index];
|
||||
const answers = [];
|
||||
['A', 'B', 'C', 'D'].forEach((k) => {
|
||||
if (q[`Antwort ${k}`]) answers.push({ key: k, text: q[`Antwort ${k}`] });
|
||||
});
|
||||
setShuffledAnswers(shuffleArray(answers));
|
||||
}, [index, questions]);
|
||||
|
||||
if (error) return <div className="min-h-screen flex items-center justify-center text-red-400 text-xl p-6 text-center">{error}</div>;
|
||||
if (!questions.length) return <div className="min-h-screen flex items-center justify-center animate-pulse text-xl text-amber-400">Lade Fragen...</div>;
|
||||
|
||||
const q = questions[index];
|
||||
const typ = (q['Typ'] || 'MC').toLowerCase();
|
||||
const sanitized = {
|
||||
frage: q['Frage'],
|
||||
typ: q['Typ'] || 'MC',
|
||||
bild: q['Bild'] && q['Bild'].length ? q['Bild'][0].url : null,
|
||||
antworten: [],
|
||||
frage: q['Frage'] || q['frage'] || '(Kein Fragetext)',
|
||||
typ: q['Typ'] || q['typ'] || 'MC',
|
||||
bild: q['Bild'] && Array.isArray(q['Bild']) && q['Bild'].length ? q['Bild'][0].url : null,
|
||||
antworten: shuffledAnswers,
|
||||
};
|
||||
['A', 'B', 'C', 'D'].forEach((k) => { if (q[`Antwort ${k}`]) sanitized.antworten.push({ key: k, text: q[`Antwort ${k}`] }); });
|
||||
|
||||
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;
|
||||
if (typ === 'freitext') {
|
||||
correct = [rawCorrect];
|
||||
// Normalize comparison
|
||||
const normalize = (t) => (t || '').toLowerCase().replace(/[^a-zäöüß0-9]/g, '').replace(/[ä]/g, 'ae').replace(/[ö]/g, 'oe').replace(/[ü]/g, 'ue').replace(/ß/g, 'ss');
|
||||
ok = normalize(selected[0]) === normalize(rawCorrect);
|
||||
ok = normalizeText(selected[0]) === normalizeText(rawCorrect);
|
||||
} else {
|
||||
correct = rawCorrect.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
|
||||
const correctSet = new Set(correct);
|
||||
@@ -433,7 +454,7 @@ function SoloQuizPage({ params, navigate }) {
|
||||
setIsCorrect(ok);
|
||||
setShowFeedback(true);
|
||||
|
||||
results.push({
|
||||
const newResults = [...results, {
|
||||
Nutzername: params.name,
|
||||
Frage_ID: q.id,
|
||||
Fragenset: params.setName,
|
||||
@@ -443,13 +464,13 @@ function SoloQuizPage({ params, navigate }) {
|
||||
Session_ID: sessionId.current,
|
||||
Modus: 'Solo',
|
||||
Zeitstempel: new Date().toISOString(),
|
||||
});
|
||||
}];
|
||||
setResults(newResults);
|
||||
};
|
||||
|
||||
const nextQuestion = () => {
|
||||
if (index + 1 >= questions.length) {
|
||||
// Save all results, then navigate
|
||||
api.post('/api/results', { answers: results }).catch(console.error);
|
||||
api.post('/api/results', { answers: results }).catch((e) => console.error('Save error:', e));
|
||||
navigate('solo-result', { results, setName: params.setName, name: params.name, questions });
|
||||
return;
|
||||
}
|
||||
@@ -462,21 +483,21 @@ function SoloQuizPage({ params, navigate }) {
|
||||
<div className="min-h-screen flex flex-col p-4 max-w-2xl mx-auto">
|
||||
<div className="text-center text-sm text-gray-400 mb-2">Frage {index + 1} / {questions.length}</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2 mb-4">
|
||||
<div className="bg-emerald-500 h-full rounded-full transition-all" style={{ width: `${((index + 1) / questions.length) * 100}%` }} />
|
||||
<div className="bg-amber-500 h-full rounded-full transition-all" style={{ width: `${((index + 1) / questions.length) * 100}%` }} />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold mb-4 text-center">{sanitized.frage}</h2>
|
||||
{sanitized.bild && <img src={sanitized.bild} className="max-h-48 mx-auto rounded-lg mb-4 cursor-pointer" alt="" onClick={() => setEnlargedImg(sanitized.bild)} />}
|
||||
<AnswerGrid question={sanitized} selected={selected} onToggle={setSelected} disabled={showFeedback} />
|
||||
{!showFeedback && (
|
||||
<button onClick={checkAnswer} disabled={selected.length === 0} className="mt-4 py-3 bg-emerald-600 hover:bg-emerald-700 disabled:opacity-40 rounded-xl text-lg font-bold w-full">Prüfen</button>
|
||||
<button onClick={checkAnswer} disabled={selected.length === 0} className="mt-4 py-3 bg-amber-600 hover:bg-amber-700 disabled:opacity-40 rounded-xl text-lg font-bold w-full shadow-lg shadow-amber-900/30">Prüfen</button>
|
||||
)}
|
||||
{showFeedback && (
|
||||
<div className="mt-4 text-center space-y-3">
|
||||
<div className={`text-2xl font-bold ${isCorrect ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{isCorrect ? '✅ Richtig!' : '❌ Falsch'}
|
||||
</div>
|
||||
{!isCorrect && <p className="bg-gray-800 py-2 px-4 rounded-lg inline-block">Richtig: <strong>{correctAnswer.join(', ')}</strong></p>}
|
||||
<button onClick={nextQuestion} className="w-full py-3 bg-indigo-600 hover:bg-indigo-700 rounded-xl text-lg font-bold">
|
||||
{!isCorrect && <p className="bg-gray-800 py-2 px-4 rounded-lg inline-block">Richtig: <strong className="text-amber-400">{correctAnswer.join(', ')}</strong></p>}
|
||||
<button onClick={nextQuestion} className="w-full py-3 bg-red-600 hover:bg-red-700 rounded-xl text-lg font-bold shadow-lg shadow-red-900/30">
|
||||
{index + 1 >= questions.length ? 'Ergebnis anzeigen' : 'Nächste Frage →'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -496,14 +517,14 @@ function SoloResultPage({ params, navigate }) {
|
||||
const correct = results.filter((r) => r.Richtig).length;
|
||||
const pct = total > 0 ? Math.round((correct / total) * 100) : 0;
|
||||
|
||||
// Group by category
|
||||
const byCategory = {};
|
||||
results.forEach((r, i) => {
|
||||
const q = questions[i];
|
||||
const cat = q?.['Kategorie'] || 'Ohne Kategorie';
|
||||
if (!byCategory[cat]) byCategory[cat] = { total: 0, correct: 0 };
|
||||
byCategory[cat].total++;
|
||||
if (r.Richtig) byCategory[cat].correct++;
|
||||
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++;
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -524,7 +545,7 @@ function SoloResultPage({ params, navigate }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full max-w-md space-y-3">
|
||||
<button onClick={() => navigate('solo-select')} className="w-full py-3 bg-emerald-600 hover:bg-emerald-700 rounded-xl font-bold">Nochmal spielen</button>
|
||||
<button onClick={() => 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</button>
|
||||
<button onClick={() => navigate('home')} className="w-full py-3 bg-gray-700 hover:bg-gray-600 rounded-xl font-bold">Zur Startseite</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -548,7 +569,6 @@ function MyResultsPage({ navigate }) {
|
||||
.catch((e) => setError(e.message));
|
||||
};
|
||||
|
||||
// Group by session
|
||||
const sessions = {};
|
||||
if (results) {
|
||||
results.forEach((r) => {
|
||||
@@ -562,8 +582,8 @@ function MyResultsPage({ navigate }) {
|
||||
<div className="min-h-screen flex flex-col items-center p-6 pt-12">
|
||||
<h2 className="text-3xl font-bold mb-6">📊 Meine Ergebnisse</h2>
|
||||
<div className="w-full max-w-md space-y-3">
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center border-2 border-gray-700 focus:border-indigo-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && search()} />
|
||||
<button onClick={search} className="w-full py-3 bg-indigo-600 hover:bg-indigo-700 rounded-xl font-bold">Suchen</button>
|
||||
<input className="w-full p-4 rounded-xl bg-gray-800 text-white text-center border-2 border-gray-700 focus:border-red-500 outline-none" placeholder="Dein Name" value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && search()} />
|
||||
<button onClick={search} className="w-full py-3 bg-red-600 hover:bg-red-700 rounded-xl font-bold shadow-lg shadow-red-900/30">Suchen</button>
|
||||
{error && <p className="text-red-400 text-center">{error}</p>}
|
||||
</div>
|
||||
{results && results.length === 0 && <p className="mt-4 text-gray-500">Keine Ergebnisse gefunden.</p>}
|
||||
@@ -577,7 +597,7 @@ function MyResultsPage({ navigate }) {
|
||||
<div key={sid} className="bg-gray-800 rounded-xl p-4">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${s.modus === 'Live' ? 'bg-indigo-600' : 'bg-emerald-600'}`}>{s.modus}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full text-white ${s.modus === 'Live' ? 'bg-red-600' : 'bg-amber-600'}`}>{s.modus}</span>
|
||||
<span className="ml-2 font-semibold">{s.fragenset}</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">{s.date ? new Date(s.date).toLocaleDateString('de') : ''}</span>
|
||||
|
||||
Reference in New Issue
Block a user