Refactor modal logic and implement ModalPortal component
Moved modal rendering logic to a new `ModalPortal` component to improve reusability and separation of concerns. Adjusted styles for better UI consistency, including improved backdrop and modal behavior. Enhanced accessibility by handling escape key events and blocking background scrolling when the modal is open.
This commit is contained in:
@@ -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