import { useState, useEffect, useCallback } from "react"; const TEACHER_CODE = "lehrer123"; const STORAGE = { WORDS: "tk_words_v2", ROUND: "tk_round_v2", RESULTS: "tk_results_v2", LEADERBOARD: "tk_leaderboard_v2", PHASE: "tk_phase_v2", ROUND_START: "tk_round_start_v2", }; // Speed bonus over 20 seconds: 10 pts (0-2s), 9 pts (2-4s), ..., 1 pt (18-20s+) const calcPoints = (elapsedMs) => { const secs = elapsedMs / 1000; return Math.max(1, 10 - Math.floor(secs / 2)); }; const fonts = `@import url('https://fonts.googleapis.com/css2?family=Baloo+2:wght@700;800;900&family=Nunito:wght@400;600;700;800&display=swap');`; export default function App() { const [role, setRole] = useState(null); const [name, setName] = useState(""); const [nameInput, setNameInput] = useState(""); const [codeInput, setCodeInput] = useState(""); // Game state (synced via storage) const [words, setWords] = useState([]); const [round, setRound] = useState(0); // 0-based index into words const [phase, setPhase] = useState("waiting"); const [roundResults, setRoundResults] = useState({}); const [leaderboard, setLeaderboard] = useState({}); const [roundStart, setRoundStart] = useState(null); // Local state const [answer, setAnswer] = useState(""); const [submitted, setSubmitted] = useState(false); const [myLastResult, setMyLastResult] = useState(null); // Admin word setup const [wordInputs, setWordInputs] = useState(Array(10).fill("")); const [setupDone, setSetupDone] = useState(false); const [actionLoading, setActionLoading] = useState(false); const [actionError, setActionError] = useState(null); const poll = useCallback(async () => { try { const [wRes, rRes, pRes, rrRes, lbRes, rsRes] = await Promise.all([ window.storage.get(STORAGE.WORDS, true).catch(() => null), window.storage.get(STORAGE.ROUND, true).catch(() => null), window.storage.get(STORAGE.PHASE, true).catch(() => null), window.storage.get(STORAGE.RESULTS, true).catch(() => null), window.storage.get(STORAGE.LEADERBOARD, true).catch(() => null), window.storage.get(STORAGE.ROUND_START, true).catch(() => null), ]); if (wRes?.value) { const w = JSON.parse(wRes.value); setWords(w); if (w.length > 0) setSetupDone(true); } if (rRes?.value) setRound(parseInt(rRes.value)); if (pRes?.value) setPhase(pRes.value); if (rrRes?.value) setRoundResults(JSON.parse(rrRes.value)); if (lbRes?.value) setLeaderboard(JSON.parse(lbRes.value)); if (rsRes?.value) setRoundStart(parseInt(rsRes.value)); } catch {} }, []); useEffect(() => { if (!role) return; poll(); const id = setInterval(poll, 1500); return () => clearInterval(id); }, [role, poll]); // Reset local answer state when round changes useEffect(() => { setAnswer(""); setSubmitted(false); setMyLastResult(null); }, [round, phase]); // ---- TEACHER ACTIONS ---- const saveWords = async () => { const filtered = wordInputs.map(w => w.trim()).filter(Boolean); if (filtered.length < 2) { alert("Bitte mindestens 2 Wörter eingeben!"); return; } await window.storage.set(STORAGE.WORDS, JSON.stringify(filtered), true); await window.storage.set(STORAGE.ROUND, "0", true); await window.storage.set(STORAGE.PHASE, "waiting", true); await window.storage.set(STORAGE.RESULTS, JSON.stringify({}), true); await window.storage.set(STORAGE.LEADERBOARD, JSON.stringify({}), true); setSetupDone(true); poll(); }; const startRound = async () => { setActionLoading(true); setActionError(null); try { const now = Date.now(); await window.storage.set(STORAGE.RESULTS, JSON.stringify({}), true); await window.storage.set(STORAGE.PHASE, "active", true); await window.storage.set(STORAGE.ROUND_START, String(now), true); // Immediately update local state so UI responds even before poll setPhase("active"); setRoundResults({}); setRoundStart(now); await poll(); } catch(e) { setActionError("Fehler: " + (e?.message || "Unbekannt")); } finally { setActionLoading(false); } }; const endRound = async () => { setActionLoading(true); setActionError(null); try { await window.storage.set(STORAGE.PHASE, "roundover", true); setPhase("roundover"); await poll(); } catch(e) { setActionError("Fehler: " + (e?.message || "Unbekannt")); } finally { setActionLoading(false); } }; const nextRound = async () => { setActionLoading(true); setActionError(null); try { const nextIdx = round + 1; if (nextIdx >= words.length) { await window.storage.set(STORAGE.PHASE, "gameover", true); setPhase("gameover"); } else { const now = Date.now(); await window.storage.set(STORAGE.ROUND, String(nextIdx), true); await window.storage.set(STORAGE.RESULTS, JSON.stringify({}), true); await window.storage.set(STORAGE.PHASE, "active", true); await window.storage.set(STORAGE.ROUND_START, String(now), true); setRound(nextIdx); setRoundResults({}); setPhase("active"); setRoundStart(now); } await poll(); } catch(e) { setActionError("Fehler: " + (e?.message || "Unbekannt")); } finally { setActionLoading(false); } }; const resetAll = async () => { if (!confirm("Alles zurücksetzen?")) return; await Promise.all(Object.values(STORAGE).map(k => window.storage.set(k, k === STORAGE.WORDS ? JSON.stringify(words) : k === STORAGE.LEADERBOARD ? JSON.stringify({}) : k === STORAGE.ROUND ? "0" : k === STORAGE.PHASE ? "waiting" : JSON.stringify({}), true).catch(() => {}))); setSetupDone(true); poll(); }; // ---- STUDENT ACTIONS ---- const submitAnswer = async () => { if (!answer.trim() || submitted || phase !== "active") return; const currentWord = words[round] || ""; const correct = answer.trim().toLowerCase() === currentWord.toLowerCase(); const now = Date.now(); const elapsed = roundStart ? now - roundStart : 9999; const points = correct ? calcPoints(elapsed) : 0; setMyLastResult({ correct, answer: answer.trim(), word: currentWord, points, elapsed }); setSubmitted(true); // Save to round results const rrRes = await window.storage.get(STORAGE.RESULTS, true).catch(() => null); const rr = rrRes?.value ? JSON.parse(rrRes.value) : {}; rr[name] = { correct, answer: answer.trim(), time: now, points }; await window.storage.set(STORAGE.RESULTS, JSON.stringify(rr), true); // Update leaderboard const lbRes = await window.storage.get(STORAGE.LEADERBOARD, true).catch(() => null); const lb = lbRes?.value ? JSON.parse(lbRes.value) : {}; if (!lb[name]) lb[name] = { points: 0, total: 0 }; lb[name].total += 1; lb[name].points = (lb[name].points || 0) + points; await window.storage.set(STORAGE.LEADERBOARD, JSON.stringify(lb), true); }; // ---- DERIVED ---- const currentWord = words[round] || ""; const resultsList = Object.entries(roundResults) .map(([n, r]) => ({ name: n, ...r })) .sort((a, b) => a.time - b.time); const lbSorted = Object.entries(leaderboard) .map(([n, s]) => ({ name: n, ...s })) .sort((a, b) => (b.points || 0) - (a.points || 0)); const medals = ["🥇", "🥈", "🥉"]; // ---- STYLES ---- const S = { page: { minHeight: "100vh", background: "#0F172A", color: "#F1F5F9", fontFamily: "'Nunito',sans-serif" }, center: { maxWidth: 520, margin: "0 auto", padding: "1.5rem 1rem" }, card: (extra = {}) => ({ background: "#1E293B", border: "1.5px solid rgba(255,255,255,0.07)", borderRadius: 16, padding: "1.25rem", marginBottom: "1rem", ...extra }), input: { width: "100%", background: "#0F172A", border: "1.5px solid rgba(255,255,255,0.12)", borderRadius: 10, padding: "0.65rem 1rem", color: "#F1F5F9", fontSize: "1rem", outline: "none", boxSizing: "border-box" }, btn: (bg, extra = {}) => ({ border: "none", borderRadius: 10, padding: "0.7rem 1.25rem", background: bg, color: "#fff", fontWeight: 800, fontSize: "0.95rem", cursor: "pointer", width: "100%", marginTop: "0.6rem", ...extra }), label: { fontSize: "0.72rem", color: "#64748B", fontWeight: 700, textTransform: "uppercase", letterSpacing: 1, marginBottom: "0.35rem", display: "block" }, }; // ---- LOGIN SCREEN ---- if (!role) return (
Diktat-Spiel für die ganze Klasse
Gib bis zu 10 Wörter ein. Mit Enter springst du zum nächsten Feld.
{phase === "gameover" ? "Spiel beendet" : `Runde ${round + 1} von ${words.length}`}
{phase !== "gameover" && ({currentWord}
)} {phase === "waiting" && ( )} {actionError &&{actionError}
} {phase === "active" && ( )} {phase === "roundover" && ( )} {phase === "gameover" && ( )}Warte auf Antworten... ⏳
: resultsList.map((r, i) => (Noch keine Punkte
: lbSorted.map((e, i) => (Die Aufgabe erscheint gleich hier!
{lbSorted.length > 0 && (Tippe dieses Wort ab:
Enter oder ✓ zum Absenden
{leaderboard[name] && (Dein Stand: {leaderboard[name].correct} Punkte aus {leaderboard[name].total} Runden
)}Du hast getippt: {myLastResult.answer}
{myLastResult.correct &&Zeit: {(myLastResult.elapsed / 1000).toFixed(1)}s
} {!myLastResult.correct &&Richtig wäre: {myLastResult.word}
} {leaderboard[name] && (Mein Gesamtstand
{leaderboard[name].points || 0} Punkte
Warte auf die nächste Runde...
Wort war:
{currentWord}
{myLastResult.correct ? "✅ Deine Antwort war richtig!" : `❌ Du hast „${myLastResult.answer}" geschrieben`}
Warte auf nächste Runde...