Refactor app by modularizing components and extracting utilities.
The changes split large features into smaller, reusable components like `AdminPanel`, `LoginPage`, `TopBar`, `PasswordModal`, and `ChipModal`. Utility functions such as `cycleTag` and `chipStorage` were extracted for better organization. This improves the code's readability, maintainability, and scalability.
This commit is contained in:
136
frontend/src/components/AdminPanel.jsx
Normal file
136
frontend/src/components/AdminPanel.jsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { api } from "../api/client";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function AdminPanel() {
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [role, setRole] = useState("user");
|
||||
const [msg, setMsg] = useState("");
|
||||
|
||||
const loadUsers = async () => {
|
||||
const u = await api("/admin/users");
|
||||
setUsers(u);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers().catch(() => {});
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
setEmail("");
|
||||
setPassword("");
|
||||
setRole("user");
|
||||
};
|
||||
|
||||
const createUser = async () => {
|
||||
setMsg("");
|
||||
try {
|
||||
await api("/admin/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password, role }),
|
||||
});
|
||||
setMsg("✅ User erstellt.");
|
||||
await loadUsers();
|
||||
resetForm();
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
setMsg("❌ Fehler: " + (e?.message || "unknown"));
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setOpen(false);
|
||||
setMsg("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.adminWrap}>
|
||||
<div style={styles.adminTop}>
|
||||
<div style={styles.adminTitle}>Admin Dashboard</div>
|
||||
<button onClick={() => setOpen(true)} style={styles.primaryBtn}>
|
||||
+ User anlegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, fontWeight: 900, color: stylesTokens.textGold }}>
|
||||
Vorhandene User
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 8, display: "grid", gap: 8 }}>
|
||||
{users.map((u) => (
|
||||
<div key={u.id} style={styles.userRow}>
|
||||
<div style={{ color: stylesTokens.textMain }}>{u.email}</div>
|
||||
<div style={{ textAlign: "center", fontWeight: 900, color: stylesTokens.textGold }}>
|
||||
{u.role}
|
||||
</div>
|
||||
<div style={{ textAlign: "center", opacity: 0.85, color: stylesTokens.textMain }}>
|
||||
{u.disabled ? "disabled" : "active"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div style={styles.modalOverlay} onMouseDown={closeModal}>
|
||||
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
||||
<div style={styles.modalHeader}>
|
||||
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>
|
||||
Neuen User anlegen
|
||||
</div>
|
||||
<button onClick={closeModal} style={styles.modalCloseBtn} aria-label="Schließen">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
|
||||
<input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
style={styles.input}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Initial Passwort"
|
||||
type="password"
|
||||
style={styles.input}
|
||||
/>
|
||||
<select value={role} onChange={(e) => setRole(e.target.value)} style={styles.input}>
|
||||
<option value="user">user</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
|
||||
{msg && <div style={{ opacity: 0.9, color: stylesTokens.textMain }}>{msg}</div>}
|
||||
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setMsg("");
|
||||
}}
|
||||
style={styles.secondaryBtn}
|
||||
>
|
||||
Leeren
|
||||
</button>
|
||||
<button onClick={createUser} style={styles.primaryBtn}>
|
||||
User erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
|
||||
Tipp: Klick auf Item: Grün → Rot → Grau → Leer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
frontend/src/components/ChipModal.jsx
Normal file
35
frontend/src/components/ChipModal.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
import { CHIP_LIST } from "../constants";
|
||||
|
||||
export default function ChipModal({ chipOpen, closeChipModalToDash, chooseChip }) {
|
||||
if (!chipOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.modalOverlay} onMouseDown={closeChipModalToDash}>
|
||||
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
||||
<div style={styles.modalHeader}>
|
||||
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Wer hat die Karte?</div>
|
||||
<button onClick={closeChipModalToDash} style={styles.modalCloseBtn} aria-label="Schließen">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, color: stylesTokens.textMain }}>Chip auswählen:</div>
|
||||
|
||||
<div style={styles.chipGrid}>
|
||||
{CHIP_LIST.map((c) => (
|
||||
<button key={c} onClick={() => chooseChip(c)} style={styles.chipBtn}>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: stylesTokens.textDim }}>
|
||||
Tipp: Wenn du wieder auf den Notiz-Button klickst, geht’s von <b>s</b> zurück auf —.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
frontend/src/components/LoginPage.jsx
Normal file
71
frontend/src/components/LoginPage.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
|
||||
export default function LoginPage({
|
||||
loginEmail,
|
||||
setLoginEmail,
|
||||
loginPassword,
|
||||
setLoginPassword,
|
||||
showPw,
|
||||
setShowPw,
|
||||
doLogin,
|
||||
}) {
|
||||
return (
|
||||
<div style={styles.loginPage}>
|
||||
<div style={styles.bgFixed} aria-hidden="true">
|
||||
<div style={styles.bgMap} />
|
||||
</div>
|
||||
|
||||
<div style={styles.candleGlowLayer} aria-hidden="true" />
|
||||
|
||||
<div style={styles.loginCard}>
|
||||
<div style={styles.loginTitle}>Zauber-Detektiv Notizbogen</div>
|
||||
|
||||
<div style={styles.loginSubtitle}>Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen</div>
|
||||
|
||||
<div style={{ marginTop: 18, display: "grid", gap: 12 }}>
|
||||
<div style={styles.loginFieldWrap}>
|
||||
<input
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
style={styles.loginInput}
|
||||
inputMode="email"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={styles.loginFieldWrap}>
|
||||
<div style={styles.inputRow}>
|
||||
<input
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
placeholder="Passwort"
|
||||
type={showPw ? "text" : "password"}
|
||||
style={styles.inputInRow}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPw((v) => !v)}
|
||||
style={styles.pwToggleBtn}
|
||||
aria-label={showPw ? "Passwort verstecken" : "Passwort anzeigen"}
|
||||
title={showPw ? "Verstecken" : "Anzeigen"}
|
||||
>
|
||||
{showPw ? "🙈" : "👁"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={doLogin} style={styles.loginBtn}>
|
||||
✦ Anmelden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={styles.loginHint}>
|
||||
Deine Notizen bleiben privat – jeder Spieler sieht nur seinen eigenen Zettel.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
frontend/src/components/PasswordModal.jsx
Normal file
66
frontend/src/components/PasswordModal.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function PasswordModal({
|
||||
pwOpen,
|
||||
closePwModal,
|
||||
pw1,
|
||||
setPw1,
|
||||
pw2,
|
||||
setPw2,
|
||||
pwMsg,
|
||||
pwSaving,
|
||||
savePassword,
|
||||
}) {
|
||||
if (!pwOpen) return null;
|
||||
|
||||
return (
|
||||
<div style={styles.modalOverlay} onMouseDown={closePwModal}>
|
||||
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
||||
<div style={styles.modalHeader}>
|
||||
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Passwort setzen</div>
|
||||
<button onClick={closePwModal} style={styles.modalCloseBtn} aria-label="Schließen">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
|
||||
<input
|
||||
value={pw1}
|
||||
onChange={(e) => setPw1(e.target.value)}
|
||||
placeholder="Neues Passwort"
|
||||
type="password"
|
||||
style={styles.input}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
value={pw2}
|
||||
onChange={(e) => setPw2(e.target.value)}
|
||||
placeholder="Neues Passwort wiederholen"
|
||||
type="password"
|
||||
style={styles.input}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") savePassword();
|
||||
}}
|
||||
/>
|
||||
|
||||
{pwMsg && <div style={{ opacity: 0.92, color: stylesTokens.textMain }}>{pwMsg}</div>}
|
||||
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
|
||||
<button onClick={closePwModal} style={styles.secondaryBtn} disabled={pwSaving}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onClick={savePassword} style={styles.primaryBtn} disabled={pwSaving}>
|
||||
{pwSaving ? "Speichern..." : "Speichern"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
|
||||
Hinweis: Mindestens 8 Zeichen empfohlen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
frontend/src/components/TopBar.jsx
Normal file
59
frontend/src/components/TopBar.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { styles } from "../styles/styles";
|
||||
import { stylesTokens } from "../styles/theme";
|
||||
|
||||
export default function TopBar({
|
||||
me,
|
||||
userMenuOpen,
|
||||
setUserMenuOpen,
|
||||
openPwModal,
|
||||
doLogout,
|
||||
newGame,
|
||||
}) {
|
||||
return (
|
||||
<div style={styles.topBar}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 900, color: stylesTokens.textGold }}>{me.email}</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>{me.role}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }} data-user-menu>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button
|
||||
onClick={() => setUserMenuOpen((v) => !v)}
|
||||
style={styles.userBtn}
|
||||
title="User Menü"
|
||||
>
|
||||
<span style={{ fontSize: 16, lineHeight: 1 }}>👤</span>
|
||||
<span>Account</span>
|
||||
<span style={{ opacity: 0.8 }}>▾</span>
|
||||
</button>
|
||||
|
||||
{userMenuOpen && (
|
||||
<div style={styles.userDropdown}>
|
||||
<button onClick={openPwModal} style={styles.userDropdownItem}>
|
||||
Passwort setzen
|
||||
</button>
|
||||
|
||||
<div style={styles.userDropdownDivider} />
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
doLogout();
|
||||
}}
|
||||
style={{ ...styles.userDropdownItem, color: "#ffb3b3" }}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={newGame} style={styles.primaryBtn}>
|
||||
+ Neues Spiel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user