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,
244 lines
9.2 KiB
React
244 lines
9.2 KiB
React
import React, { useState } from "react";
|
|
import { styles } from "../styles/styles";
|
|
import { stylesTokens } from "../styles/theme";
|
|
import { useLanguage } from "../i18n";
|
|
|
|
export default function GamePickerCard({
|
|
games,
|
|
gameId,
|
|
setGameId,
|
|
onOpenHelp,
|
|
members = [],
|
|
me,
|
|
hostUserId,
|
|
isHost = false,
|
|
started = false,
|
|
finished = false,
|
|
winnerName = "",
|
|
chipCount = 0,
|
|
onStartGame,
|
|
}) {
|
|
const { language, t } = useLanguage();
|
|
const cur = games.find((x) => x.id === gameId);
|
|
const [starting, setStarting] = useState(false);
|
|
const [startError, setStartError] = useState("");
|
|
|
|
const renderMemberName = (m) => {
|
|
const base = ((m.display_name || "").trim() || (m.email || "").trim() || "—");
|
|
const isMe = !!(me?.id && String(me.id) === String(m.id));
|
|
const isHost = !!(hostUserId && String(hostUserId) === String(m.id));
|
|
|
|
const suffix = `${isHost ? " " : ""}${isMe ? " (du)" : ""}`;
|
|
return base + suffix;
|
|
};
|
|
|
|
const pillStyle = (isHost, isMe) => ({
|
|
padding: "7px 10px",
|
|
borderRadius: 999,
|
|
border: `1px solid ${
|
|
isHost ? "rgba(233,216,166,0.35)" : "rgba(233,216,166,0.16)"
|
|
}`,
|
|
background: isHost
|
|
? "linear-gradient(180deg, rgba(233,216,166,0.14), rgba(10,10,12,0.35))"
|
|
: "rgba(10,10,12,0.30)",
|
|
color: stylesTokens.textMain,
|
|
fontSize: 13,
|
|
fontWeight: 950,
|
|
boxShadow: isHost ? "0 8px 18px rgba(0,0,0,0.25)" : "none",
|
|
opacity: isMe ? 1 : 0.95,
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: 6,
|
|
whiteSpace: "nowrap",
|
|
});
|
|
|
|
const handleStartGame = async () => {
|
|
if (!onStartGame || starting || members.length < 2) return;
|
|
if (!window.confirm(language === "en" ? "Start the game now? No more players can join afterwards." : "Spiel jetzt starten? Danach können keine weiteren Spieler beitreten.")) return;
|
|
|
|
setStarting(true);
|
|
setStartError("");
|
|
try {
|
|
await onStartGame();
|
|
} catch (e) {
|
|
setStartError(e?.message || (language === "en" ? "The game could not be started." : "Das Spiel konnte nicht gestartet werden."));
|
|
} finally {
|
|
setStarting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={{ marginTop: 14 }}>
|
|
<div style={styles.card}>
|
|
<div style={styles.sectionHeader}>{t("game")}</div>
|
|
|
|
<div style={styles.cardBody}>
|
|
<select
|
|
value={gameId || ""}
|
|
onChange={(e) => setGameId(e.target.value)}
|
|
style={{ ...styles.input, flex: 1 }}
|
|
>
|
|
{games.map((g) => (
|
|
<option key={g.id} value={g.id}>
|
|
{g.name} {g.code ? `• ${g.code}` : ""}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<button onClick={onOpenHelp} style={styles.helpBtn} title={t("help")}>
|
|
{t("help")}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Code Zeile */}
|
|
{cur?.code && (
|
|
<div
|
|
style={{
|
|
padding: "0 12px 10px",
|
|
fontSize: 12,
|
|
color: stylesTokens.textDim,
|
|
opacity: 0.92,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: 10,
|
|
}}
|
|
>
|
|
<div>
|
|
Code: <b style={{ color: stylesTokens.textGold }}>{cur.code}</b>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!started ? (
|
|
<div
|
|
className="hp-lobby"
|
|
style={{
|
|
margin: "0 12px 12px",
|
|
padding: 12,
|
|
borderRadius: 16,
|
|
border: `1px solid ${stylesTokens.panelBorder}`,
|
|
background: "rgba(8,8,11,0.28)",
|
|
}}
|
|
>
|
|
<div className="hp-lobby-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
|
|
<div>
|
|
<div style={{ color: stylesTokens.textGold, fontWeight: 1000, fontSize: 15 }}>
|
|
{t("lobby")}
|
|
</div>
|
|
<div style={{ marginTop: 3, color: stylesTokens.textDim, fontSize: 12 }}>
|
|
{isHost ? (language === "en" ? "You are the host of this game." : "Du bist der Host dieses Spiels.") : (language === "en" ? "You joined the lobby." : "Du bist der Lobby beigetreten.")}
|
|
</div>
|
|
</div>
|
|
<div style={{ textAlign: "right", color: stylesTokens.textGold, fontWeight: 1000 }}>
|
|
<div style={{ fontSize: 20, lineHeight: 1 }}>{members.length}</div>
|
|
<div style={{ fontSize: 11, color: stylesTokens.textDim, fontWeight: 700 }}>{t("players")}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="hp-lobby-slots" style={{ marginTop: 12, display: "grid", gap: 6 }}>
|
|
{members.length > 0 ? members.map((m, index) => {
|
|
const isMe = String(me?.id) === String(m.id);
|
|
const isMemberHost = String(hostUserId) === String(m.id);
|
|
const name = ((m.display_name || "").trim() || (m.email || "").trim() || t("players"));
|
|
return (
|
|
<div key={m.id} className="hp-lobby-player" style={{ display: "flex", alignItems: "center", gap: 9, padding: "8px 10px", borderRadius: 11, background: "rgba(255,255,255,0.055)", border: "1px solid rgba(233,216,166,0.10)" }}>
|
|
<span style={{ color: stylesTokens.textDim, fontSize: 12, minWidth: 16 }}>{index + 1}</span>
|
|
<span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: stylesTokens.textMain, fontWeight: 900 }}>
|
|
{name}{isMe ? " (du)" : ""}
|
|
</span>
|
|
{isMemberHost && <span title="Host" style={{ color: stylesTokens.textGold }}>👑</span>}
|
|
</div>
|
|
);
|
|
}) : (
|
|
<div style={{ padding: "10px 4px", color: stylesTokens.textDim, fontSize: 13 }}>
|
|
{language === "en" ? "Waiting for players …" : "Warte auf Spieler …"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{isHost ? (
|
|
<>
|
|
<button
|
|
onClick={handleStartGame}
|
|
style={{ ...styles.primaryBtn, width: "100%", marginTop: 12 }}
|
|
disabled={starting || members.length < 2}
|
|
>
|
|
{starting ? (language === "en" ? "Starting game …" : "Spiel wird gestartet …") : `▶ ${t("startGame")}`}
|
|
</button>
|
|
<div style={{ marginTop: 7, textAlign: "center", color: stylesTokens.textDim, fontSize: 11 }}>
|
|
{members.length < 2 ? t("atLeastTwoPlayers") : t("chipsOnStart")}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div style={{ marginTop: 11, padding: "9px 10px", borderRadius: 11, background: "rgba(233,216,166,0.07)", color: stylesTokens.textDim, fontSize: 12, textAlign: "center" }}>
|
|
{t("waitingForHost")}
|
|
</div>
|
|
)}
|
|
|
|
{startError && <div style={{ marginTop: 8, color: "#ffb3b3", fontSize: 12 }}>{startError}</div>}
|
|
</div>
|
|
) : (
|
|
<div style={{ margin: "0 12px 12px", padding: "10px 12px", borderRadius: 13, border: `1px solid ${stylesTokens.panelBorder}`, background: finished ? "rgba(233,216,166,0.09)" : "rgba(124,255,182,0.07)", color: stylesTokens.textDim, fontSize: 12 }}>
|
|
{finished
|
|
? `🏆 ${t("finished")}${winnerName ? ` · ${t("winner")}: ${winnerName}` : ""}`
|
|
: `✓ ${t("started")} · ${chipCount} ${t("chipsCreated")}`}
|
|
</div>
|
|
)}
|
|
|
|
{/* Spieler */}
|
|
{started && members?.length > 0 && (
|
|
<div style={{ padding: "0 12px 12px" }}>
|
|
<div
|
|
style={{
|
|
fontSize: 12,
|
|
opacity: 0.85,
|
|
color: stylesTokens.textDim,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: 10,
|
|
}}
|
|
>
|
|
<div>{t("players")}</div>
|
|
<div style={{ fontWeight: 900, color: stylesTokens.textGold }}>
|
|
{members.length}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
marginTop: 8,
|
|
padding: 10,
|
|
borderRadius: 16,
|
|
border: `1px solid rgba(233,216,166,0.10)`,
|
|
background: "rgba(10,10,12,0.18)",
|
|
display: "flex",
|
|
flexWrap: "wrap",
|
|
gap: 8,
|
|
}}
|
|
>
|
|
{members.map((m) => {
|
|
const isMe = !!(me?.id && String(me.id) === String(m.id));
|
|
const isHost = !!(hostUserId && String(hostUserId) === String(m.id));
|
|
const label = renderMemberName(m);
|
|
|
|
return (
|
|
<div key={m.id} style={pillStyle(isHost, isMe)} title={label}>
|
|
{isHost && <span style={{ color: stylesTokens.textGold }}>👑</span>}
|
|
<span>{label.replace(" 👑", "")}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style={{ marginTop: 6, fontSize: 11, opacity: 0.7, color: stylesTokens.textDim }}>
|
|
👑 = Host
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|