dev #4

Merged
nessi merged 25 commits from dev into main 2026-02-06 13:36:47 +00:00
3 changed files with 127 additions and 75 deletions
Showing only changes of commit 52ace41ac4 - Show all commits

View File

@@ -2,7 +2,19 @@ import React, { useEffect, useState } from "react";
import { api } from "../api/client"; import { api } from "../api/client";
import { styles } from "../styles/styles"; import { styles } from "../styles/styles";
import { stylesTokens } from "../styles/theme"; import { stylesTokens } from "../styles/theme";
import { createPortal } from "react-dom";
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = prev;
};
}, [open]);
export default function AdminPanel() { export default function AdminPanel() {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
@@ -112,71 +124,74 @@ export default function AdminPanel() {
))} ))}
</div> </div>
{open && ( {open &&
<div style={styles.modalOverlay} onMouseDown={closeModal}> createPortal(
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}> <div style={styles.modalOverlay} onMouseDown={closeModal}>
<div style={styles.modalHeader}> <div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}> <div style={styles.modalHeader}>
Neuen User anlegen <div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>
</div> Neuen User anlegen
<button onClick={closeModal} style={styles.modalCloseBtn} aria-label="Schließen"> </div>
<button onClick={closeModal} style={styles.modalCloseBtn} aria-label="Schließen">
</button>
</div>
<div style={{ marginTop: 12, display: "grid", gap: 8 }}>
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Name (z.B. Sascha)"
style={styles.input}
autoFocus
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
style={styles.input}
/>
<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> </button>
</div> </div>
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}> <div style={{ marginTop: 12, display: "grid", gap: 8 }}>
Tipp: Name wird in TopBar & Siegeranzeige genutzt. <input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Name (z.B. Sascha)"
style={styles.input}
autoFocus
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
style={styles.input}
/>
<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: Name wird in TopBar & Siegeranzeige genutzt.
</div>
</div> </div>
</div> </div>
</div> </div>,
</div> document.body
)} )
}
</div> </div>
); );
} }

View File

@@ -0,0 +1,40 @@
import React, { useEffect } from "react";
import { createPortal } from "react-dom";
import { styles } from "../styles/styles";
export default function ModalPortal({ open, onClose, children }) {
useEffect(() => {
if (!open) return;
const onKeyDown = (e) => {
if (e.key === "Escape") onClose?.();
};
// Scroll der Seite sperren
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = prev;
};
}, [open, onClose]);
if (!open) return null;
return createPortal(
<div
style={styles.modalOverlay}
onMouseDown={(e) => {
// Klick außerhalb schließt
if (e.target === e.currentTarget) onClose?.();
}}
>
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}

View File

@@ -188,32 +188,29 @@ export const styles = {
// Modal // Modal
modalOverlay: { modalOverlay: {
position: "fixed", position: "fixed",
inset: 0, top: 0,
background: "rgba(0,0,0,0.78)", // stärker abdunkeln left: 0,
backdropFilter: "blur(6px)", // Hintergrund weich (macht viel aus) right: 0,
bottom: 0,
width: "100vw",
height: "100vh",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
padding: 16, padding: 16,
zIndex: 9999, zIndex: 2147483647, // wirklich ganz oben
animation: "fadeIn 160ms ease-out", background: "rgba(0,0,0,0.72)",
overflowY: "auto", // falls Viewport zu klein overflowY: "auto",
}, },
modalCard: { modalCard: {
width: "100%", width: "min(560px, 100%)",
maxWidth: 560,
borderRadius: 18, borderRadius: 18,
border: `1px solid rgba(233,216,166,0.18)`, border: `1px solid rgba(233,216,166,0.18)`,
background: "linear-gradient(180deg, rgba(20,20,24,0.95), rgba(12,12,14,0.92))", background: "rgba(12,12,14,0.96)",
boxShadow: "0 18px 55px rgba(0,0,0,0.70)", boxShadow: "0 18px 55px rgba(0,0,0,0.70)",
padding: 14, padding: 14,
backdropFilter: "blur(8px)", maxHeight: "calc(100vh - 32px)",
animation: "popIn 160ms ease-out",
color: stylesTokens.textMain,
// neu: damit es nie “kaputt” aussieht
maxHeight: "calc(100dvh - 32px)",
overflow: "auto", overflow: "auto",
}, },
modalHeader: { modalHeader: {