Files
cluedo-hp-webapp/frontend/src/components/GamePickerCard.jsx
T
nessi 33a972e502 Add internationalization support with English/German language toggle across all components
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,
2026-08-02 10:33:40 +02:00

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>
);
}