Integrated i18n system throughout the application with `useLanguage` hook providing translation function and language state. Added bilingual support to all user-facing strings in App.jsx (password validation, error messages, game creation, sheet sections), AdminPanel (user management, form validation, status messages), AdminSettingsModal (SMTP configuration), ChipModal, DesignModal, GamePickerCard (lobby interface,
268 lines
8.9 KiB
React
268 lines
8.9 KiB
React
import React, { useEffect, useMemo, useState } from "react";
|
|
import { styles } from "../styles/styles";
|
|
import { stylesTokens } from "../styles/theme";
|
|
import { useLanguage } from "../i18n";
|
|
|
|
export default function NewGameModal({
|
|
open,
|
|
onClose,
|
|
onCreate,
|
|
onJoin,
|
|
|
|
// ✅ neu:
|
|
currentCode = "",
|
|
gameFinished = false,
|
|
hasGame = false,
|
|
}) {
|
|
const { language, t } = useLanguage();
|
|
// modes: running | choice | create | join
|
|
const [mode, setMode] = useState("choice");
|
|
const [joinCode, setJoinCode] = useState("");
|
|
const [err, setErr] = useState("");
|
|
const [created, setCreated] = useState(null); // { code }
|
|
const [toast, setToast] = useState("");
|
|
|
|
const canJoin = useMemo(() => joinCode.trim().length >= 4, [joinCode]);
|
|
|
|
// ✅ wichtig: beim Öffnen entscheidet der Modus anhand "läuft vs beendet"
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
setErr("");
|
|
setToast("");
|
|
setJoinCode("");
|
|
setCreated(null);
|
|
|
|
// Wenn ein Spiel läuft (und nicht finished) -> nur Code anzeigen
|
|
if (hasGame && !gameFinished) {
|
|
setMode("running");
|
|
} else {
|
|
setMode("choice");
|
|
}
|
|
}, [open, hasGame, gameFinished]);
|
|
|
|
if (!open) return null;
|
|
|
|
const showToast = (msg) => {
|
|
setToast(msg);
|
|
setTimeout(() => setToast(""), 1100);
|
|
};
|
|
|
|
const doCreate = async () => {
|
|
setErr("");
|
|
try {
|
|
const res = await onCreate();
|
|
setCreated({ code: res.code });
|
|
setMode("create");
|
|
} catch (e) {
|
|
setErr("❌ " + (language === "en" ? "Error: " : "Fehler: ") + (e?.message || "unknown"));
|
|
}
|
|
};
|
|
|
|
const doJoin = async () => {
|
|
setErr("");
|
|
try {
|
|
await onJoin(joinCode.trim().toUpperCase());
|
|
onClose();
|
|
} catch (e) {
|
|
setErr("❌ " + (language === "en" ? "Error: " : "Fehler: ") + (e?.message || "unknown"));
|
|
}
|
|
};
|
|
|
|
const copyText = async (text, okMsg = language === "en" ? "✅ Code copied" : "✅ Code kopiert") => {
|
|
try {
|
|
await navigator.clipboard.writeText(text || "");
|
|
showToast(okMsg);
|
|
} catch {
|
|
showToast(language === "en" ? "❌ Copy failed" : "❌ Copy nicht möglich");
|
|
}
|
|
};
|
|
|
|
const codeToShow =
|
|
(created?.code || "").trim() ||
|
|
(currentCode || "").trim();
|
|
|
|
return (
|
|
<div style={styles.modalOverlay} onMouseDown={onClose}>
|
|
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
|
<div style={styles.modalHeader}>
|
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>
|
|
{t("game")}
|
|
</div>
|
|
<button onClick={onClose} style={styles.modalCloseBtn} aria-label={t("close")}>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{/* Toast */}
|
|
{toast && (
|
|
<div
|
|
style={{
|
|
marginTop: 10,
|
|
padding: "10px 12px",
|
|
borderRadius: 12,
|
|
border: `1px solid ${stylesTokens.panelBorder}`,
|
|
background: stylesTokens.panelBg,
|
|
color: stylesTokens.textMain,
|
|
fontWeight: 900,
|
|
textAlign: "center",
|
|
animation: "fadeIn 120ms ease-out",
|
|
}}
|
|
>
|
|
{toast}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
|
|
{/* ✅ RUNNING: Nur Code anzeigen, keine Choice */}
|
|
{mode === "running" && (
|
|
<>
|
|
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
|
{language === "en" ? <>The game is in progress. Here is the <b>join code</b>:</> : <>Das Spiel läuft noch. Hier ist der <b>Join-Code</b>:</>}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gap: 8,
|
|
padding: 12,
|
|
borderRadius: 16,
|
|
border: `1px solid ${stylesTokens.panelBorder}`,
|
|
background: stylesTokens.panelBg,
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
|
|
{t("gameCode")}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
fontSize: 28,
|
|
fontWeight: 1100,
|
|
letterSpacing: 2,
|
|
color: stylesTokens.textGold,
|
|
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
|
|
}}
|
|
>
|
|
{codeToShow || "—"}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => copyText(codeToShow)}
|
|
style={styles.primaryBtn}
|
|
disabled={!codeToShow}
|
|
title={!codeToShow ? (language === "en" ? "No code available" : "Kein Code verfügbar") : (language === "en" ? "Copy code" : "Code kopieren")}
|
|
>
|
|
⧉ {t("copy")} {language === "en" ? "code" : "Code"}
|
|
</button>
|
|
</div>
|
|
|
|
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
|
|
{language === "en" ? "Once a winner is selected, you can create or join a new game here." : "Sobald ein Sieger gesetzt wurde, kannst du hier ein neues Spiel erstellen oder beitreten."}
|
|
</div>
|
|
|
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
<button onClick={onClose} style={styles.primaryBtn}>
|
|
{language === "en" ? "Done" : "Fertig"}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
|
|
{/* ✅ CHOICE: nur wenn Spiel beendet oder kein Spiel selected */}
|
|
{mode === "choice" && (
|
|
<>
|
|
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
|
{language === "en" ? <>Would you like to <b>create</b> a game or <b>join</b> one?</> : <>Willst du ein Spiel <b>erstellen</b> oder einem Spiel <b>beitreten</b>?</>}
|
|
</div>
|
|
|
|
<button onClick={doCreate} style={styles.primaryBtn}>
|
|
✦ {t("createGame")}
|
|
</button>
|
|
|
|
<button onClick={() => setMode("join")} style={styles.secondaryBtn}>
|
|
⎆ {t("joinGame")}
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{mode === "join" && (
|
|
<>
|
|
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
|
{language === "en" ? <>Enter the <b>code</b>:</> : <>Gib den <b>Code</b> ein:</>}
|
|
</div>
|
|
|
|
<input
|
|
value={joinCode}
|
|
onChange={(e) => setJoinCode(e.target.value.toUpperCase())}
|
|
placeholder="z.B. 8K3MZQ"
|
|
style={styles.input}
|
|
autoFocus
|
|
/>
|
|
|
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
<button onClick={() => setMode("choice")} style={styles.secondaryBtn}>
|
|
{language === "en" ? "Back" : "Zurück"}
|
|
</button>
|
|
<button onClick={doJoin} style={styles.primaryBtn} disabled={!canJoin}>
|
|
{t("join")}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{mode === "create" && created && (
|
|
<>
|
|
<div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>
|
|
{language === "en" ? "Your game was created. This code will remain visible in your old games:" : "Dein Spiel wurde erstellt. Dieser Code bleibt auch bei „Alte Spiele“ sichtbar:"}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gap: 8,
|
|
padding: 12,
|
|
borderRadius: 16,
|
|
border: `1px solid ${stylesTokens.panelBorder}`,
|
|
background: stylesTokens.panelBg,
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
|
|
{t("gameCode")}
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
fontSize: 28,
|
|
fontWeight: 1100,
|
|
letterSpacing: 2,
|
|
color: stylesTokens.textGold,
|
|
fontFamily: '"Cinzel Decorative", "IM Fell English", system-ui',
|
|
}}
|
|
>
|
|
{created.code}
|
|
</div>
|
|
|
|
<button onClick={() => copyText(created?.code || "")} style={styles.primaryBtn}>
|
|
⧉ {t("copy")} {language === "en" ? "code" : "Code"}
|
|
</button>
|
|
</div>
|
|
|
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
<button onClick={onClose} style={styles.primaryBtn}>
|
|
{language === "en" ? "Done" : "Fertig"}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{err && <div style={{ color: stylesTokens.textMain, opacity: 0.92 }}>{err}</div>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|