const { useState, useEffect, useRef } = React; function wsUrl() { return `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}`; } function PresentApp() { const [phase, setPhase] = useState('connect'); const [session, setSession] = useState(null); const [question, setQuestion] = useState(null); const [qIndex, setQIndex] = useState(0); const [qTotal, setQTotal] = useState(0); const [timeLimit, setTimeLimit] = useState(0); const [timeLeft, setTimeLeft] = useState(0); const [answerCount, setAnswerCount] = useState(0); const [result, setResult] = useState(null); const [leaderboard, setLeaderboard] = useState([]); const [error, setError] = useState(''); const timerRef = useRef(null); useEffect(() => { const code = location.hash.replace('#', ''); const params = new URLSearchParams(location.search); // Accept both ?pt= (present token) and ?token= (legacy/admin password) const token = params.get('pt') || params.get('token'); if (!code || !token) { setPhase('need-config'); return; } const ws = new WebSocket(wsUrl()); ws.onopen = () => { ws.send(JSON.stringify({ type: 'present-join', token, code })); }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); switch (msg.type) { case 'present-joined': setSession(msg.session); setLeaderboard(msg.session.leaderboard || []); setPhase('waiting'); break; case 'player-joined': case 'player-left': setSession((s) => s ? { ...s, players: msg.players, playerCount: msg.playerCount } : s); break; case 'game-start': setQTotal(msg.totalQuestions); break; case 'question': setQuestion(msg.question); setQIndex(msg.index); setQTotal(msg.total); setTimeLimit(msg.timeLimit); setTimeLeft(msg.timeLimit); setAnswerCount(0); setResult(null); setPhase('question'); break; case 'answer-count': setAnswerCount(msg.count); break; case 'time-up': clearInterval(timerRef.current); break; case 'question-result': setResult(msg); setLeaderboard(msg.leaderboard); setPhase('result'); clearInterval(timerRef.current); break; case 'game-end': setLeaderboard(msg.leaderboard); setPhase('final'); clearInterval(timerRef.current); break; case 'error': setError(msg.message); setPhase('error'); break; } }; ws.onerror = () => setPhase('error'); return () => ws.close(); }, []); useEffect(() => { clearInterval(timerRef.current); if (phase === 'question' && timeLimit > 0) { const start = Date.now(); timerRef.current = setInterval(() => { setTimeLeft(Math.max(0, timeLimit - (Date.now() - start) / 1000)); }, 100); } }, [phase, timeLimit]); if (phase === 'need-config') { return (
🚨

Quizalarm — Präsentation

Öffne diesen Link aus dem Admin-Panel.

); } if (phase === 'error') return (

❌ Verbindungsfehler

{error &&

{error}

}
); if (phase === 'connect') return
Verbinde...
; if (phase === 'waiting') return (
🚨

Quizalarm

{session?.setName}

{session?.code}

{session?.players?.length || 0} Teilnehmer

{(session?.players || []).map((n) => ( {n} ))}
); if (phase === 'question') { const pct = timeLimit > 0 ? (timeLeft / timeLimit) * 100 : 100; const barColor = pct > 50 ? 'bg-green-500' : pct > 25 ? 'bg-yellow-500' : 'bg-red-500'; return (
Frage {qIndex + 1} / {qTotal} {answerCount} / {session?.players?.length || 0} 💬 {timeLimit > 0 && {Math.ceil(timeLeft)}s}
{timeLimit > 0 && (
)}

{question?.frage}

{question?.bild && } {question?.typ?.toLowerCase() !== 'freitext' && (
{(question?.antworten || []).map((a) => { const colors = { A: 'bg-red-600', B: 'bg-blue-600', C: 'bg-amber-600', D: 'bg-green-600' }; return (
{a.text}
); })}
)} {question?.typ?.toLowerCase() === 'freitext' && (
✏️ Freitext-Antwort
)}
); } if (phase === 'result') { const correctKeys = new Set(result?.correctAnswer || []); return (

Frage {qIndex + 1} / {qTotal}

{question?.frage}

{question?.bild && } {question?.typ?.toLowerCase() !== 'freitext' && (
{(question?.antworten || []).map((a) => { const isCorrect = correctKeys.has(a.key); const count = result?.stats?.[a.key] || 0; const cls = isCorrect ? 'bg-green-600 ring-4 ring-green-400' : 'bg-gray-700 opacity-50'; return (
{isCorrect ? '✓ ' : ''}{a.text} {count}
); })}
)} {question?.typ?.toLowerCase() === 'freitext' && (
✓ {result?.correctAnswer?.[0]}
)}

🏆 Leaderboard

{leaderboard.slice(0, 8).map((e, i) => (
{i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${e.rank}.`} {e.name} {e.score}
))}
); } if (phase === 'final') return (

Quiz beendet!

{leaderboard.map((e, i) => (
{i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${e.rank}.`} {e.name} {e.score}
))}
); return
Verbinde...
; } ReactDOM.createRoot(document.getElementById('root')).render();