Massive feature update: Solo session recovery, immediate result saving, image proxy, player reconnection (Live), and UI/UX refinements
This commit is contained in:
+47
-25
@@ -4,8 +4,6 @@ function wsUrl() {
|
||||
return `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}`;
|
||||
}
|
||||
|
||||
const COLORS = { A: 'answer-a', B: 'answer-b', C: 'answer-c', D: 'answer-d' };
|
||||
|
||||
function PresentApp() {
|
||||
const [phase, setPhase] = useState('connect');
|
||||
const [session, setSession] = useState(null);
|
||||
@@ -19,7 +17,6 @@ function PresentApp() {
|
||||
const [leaderboard, setLeaderboard] = useState([]);
|
||||
const [error, setError] = useState('');
|
||||
const timerRef = useRef(null);
|
||||
const wsRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const code = location.hash.replace('#', '');
|
||||
@@ -32,7 +29,6 @@ function PresentApp() {
|
||||
}
|
||||
|
||||
const ws = new WebSocket(wsUrl());
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ type: 'present-join', token, code }));
|
||||
@@ -88,7 +84,6 @@ function PresentApp() {
|
||||
};
|
||||
|
||||
ws.onerror = () => setPhase('error');
|
||||
ws.onclose = () => { if (phase !== 'final') setPhase('error'); };
|
||||
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
@@ -106,9 +101,9 @@ function PresentApp() {
|
||||
if (phase === 'need-config') {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<img src="/img/logo.png" alt="Quizalarm" className="w-24 h-24 mb-2" />
|
||||
<div className="text-6xl mb-4">🚨</div>
|
||||
<h1 className="text-3xl font-bold mb-4 fire-gradient">Quizalarm — Präsentation</h1>
|
||||
<p className="text-gray-400 mb-4 text-center">Öffne diesen Link aus dem Admin-Panel.<br />Die URL muss <code className="bg-gray-800 px-2 py-1 rounded">?token=PASSWORT#CODE</code> enthalten.</p>
|
||||
<p className="text-gray-400 mb-4 text-center">Öffne diesen Link aus dem Admin-Panel.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,7 +119,7 @@ function PresentApp() {
|
||||
|
||||
if (phase === 'waiting') return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<img src="/img/logo.png" alt="Quizalarm" className="w-24 h-24 mb-2" />
|
||||
<div className="text-6xl mb-4">🚨</div>
|
||||
<h1 className="text-4xl font-extrabold mb-4 fire-gradient">Quizalarm</h1>
|
||||
<p className="text-gray-400 text-xl mb-6">{session?.setName}</p>
|
||||
<div className="text-8xl font-mono font-extrabold tracking-widest text-red-500 mb-8">{session?.code}</div>
|
||||
@@ -152,15 +147,18 @@ function PresentApp() {
|
||||
<div className={`${barColor} h-full rounded-full transition-all duration-200`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-4xl font-bold text-center mb-8 flex-shrink-0">{question?.frage}</h2>
|
||||
<h2 className="text-4xl font-bold text-center mb-8">{question?.frage}</h2>
|
||||
{question?.bild && <img src={question.bild} className="max-h-64 mx-auto rounded-xl mb-8" alt="" />}
|
||||
{question?.typ?.toLowerCase() !== 'freitext' && (
|
||||
<div className="grid grid-cols-2 gap-4 flex-1 max-h-96">
|
||||
{(question?.antworten || []).map((a) => (
|
||||
<div key={a.key} className={`${COLORS[a.key]} rounded-2xl flex items-center justify-center p-6 text-2xl font-bold`}>
|
||||
{a.text}
|
||||
</div>
|
||||
))}
|
||||
{(question?.antworten || []).map((a) => {
|
||||
const colors = { A: 'bg-red-600', B: 'bg-blue-600', C: 'bg-amber-600', D: 'bg-green-600' };
|
||||
return (
|
||||
<div key={a.key} className={`${colors[a.key] || 'bg-gray-600'} rounded-2xl flex items-center justify-center p-6 text-2xl font-bold`}>
|
||||
{a.text}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{question?.typ?.toLowerCase() === 'freitext' && (
|
||||
@@ -171,20 +169,44 @@ function PresentApp() {
|
||||
}
|
||||
|
||||
if (phase === 'result') {
|
||||
const maxStat = Math.max(1, ...Object.values(result?.stats || {}));
|
||||
const correctKeys = new Set(result?.correctAnswer || []);
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col p-8">
|
||||
<h2 className="text-3xl font-bold text-center mb-2">Ergebnis</h2>
|
||||
<p className="text-center text-xl text-amber-400 mb-6">Richtig: {result?.correctAnswer?.join(', ')}</p>
|
||||
<div className="flex gap-4 justify-center items-end mb-8 h-48">
|
||||
{Object.entries(result?.stats || {}).map(([k, v]) => (
|
||||
<div key={k} className="flex flex-col items-center flex-1 max-w-32">
|
||||
<span className="text-xl font-bold mb-1">{v}</span>
|
||||
<div className={`${COLORS[k]} w-full rounded-t-lg transition-all`} style={{ height: `${(v / maxStat) * 150}px`, minHeight: '8px' }} />
|
||||
<span className="text-lg font-bold mt-1">{k}</span>
|
||||
<h2 className="text-2xl text-gray-400 text-center mb-2">Frage {qIndex + 1} / {qTotal}</h2>
|
||||
<h2 className="text-3xl font-bold text-center mb-6">{question?.frage}</h2>
|
||||
{question?.bild && <img src={question.bild} className="max-h-48 mx-auto rounded-xl mb-6" alt="" />}
|
||||
|
||||
{/* Answer options with feedback colors + vote counts */}
|
||||
{question?.typ?.toLowerCase() !== 'freitext' && (
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
{(question?.antworten || []).map((a) => {
|
||||
const isCorrect = correctKeys.has(a.key);
|
||||
const count = result?.stats?.[a.key] || 0;
|
||||
let cls;
|
||||
if (isCorrect) {
|
||||
cls = 'bg-green-600 ring-4 ring-green-400';
|
||||
} else {
|
||||
cls = 'bg-gray-700 opacity-50';
|
||||
}
|
||||
return (
|
||||
<div key={a.key} className={`${cls} rounded-2xl p-6 text-2xl font-bold flex items-center justify-between`}>
|
||||
<span>{isCorrect ? '✓ ' : ''}{a.text}</span>
|
||||
<span className="text-3xl ml-4 opacity-80">{count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{question?.typ?.toLowerCase() === 'freitext' && (
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-block bg-green-600 ring-4 ring-green-400 rounded-2xl px-8 py-4 text-2xl font-bold">
|
||||
✓ {result?.correctAnswer?.[0]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Leaderboard */}
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-center mb-4">🏆 Leaderboard</h3>
|
||||
<div className="max-w-xl mx-auto">
|
||||
|
||||
Reference in New Issue
Block a user