dev #4
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
40
frontend/src/components/ModalPortal.jsx
Normal file
40
frontend/src/components/ModalPortal.jsx
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user