Add user editing functionality to admin panel with improved UI and self-protection safeguards
Implemented comprehensive user editing in admin dashboard with PATCH endpoint for updating email, display name, role, password, and disabled status. Added validation to prevent admins from demoting or disabling themselves, and duplicate email detection. Refactored AdminPanel to modal-based editor with separate create/edit modes, form state management, and save/cancel actions. Enhanced UI with field labels, checkbox for account status, action
This commit is contained in:
@@ -61,11 +61,48 @@ def delete_user(req: Request, user_id: str, db: Session = Depends(get_db)):
|
|||||||
if not u:
|
if not u:
|
||||||
raise HTTPException(404, "not found")
|
raise HTTPException(404, "not found")
|
||||||
|
|
||||||
if u.role == Role.admin.value:
|
|
||||||
raise HTTPException(400, "cannot delete admin user")
|
|
||||||
|
|
||||||
# soft delete
|
# soft delete
|
||||||
u.disabled = True
|
u.disabled = True
|
||||||
db.add(u)
|
db.add(u)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/users/{user_id}")
|
||||||
|
def update_user(req: Request, user_id: str, data: dict, db: Session = Depends(get_db)):
|
||||||
|
admin = require_admin(req, db)
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(404, "not found")
|
||||||
|
|
||||||
|
email = (data.get("email") or user.email).lower().strip()
|
||||||
|
display_name = (data.get("display_name") if "display_name" in data else user.display_name or "").strip()
|
||||||
|
role = data.get("role") or user.role
|
||||||
|
password = data.get("password") or ""
|
||||||
|
|
||||||
|
if not email or "@" not in email:
|
||||||
|
raise HTTPException(400, "valid email required")
|
||||||
|
if role not in (Role.admin.value, Role.user.value):
|
||||||
|
raise HTTPException(400, "invalid role")
|
||||||
|
if password and len(password) < 8:
|
||||||
|
raise HTTPException(400, "password too short (min 8)")
|
||||||
|
if admin.id == user_id and role != Role.admin.value:
|
||||||
|
raise HTTPException(400, "cannot demote yourself")
|
||||||
|
if admin.id == user_id and data.get("disabled") is True:
|
||||||
|
raise HTTPException(400, "cannot disable yourself")
|
||||||
|
|
||||||
|
duplicate = db.query(User).filter(User.email == email, User.id != user_id).first()
|
||||||
|
if duplicate:
|
||||||
|
raise HTTPException(409, "email exists")
|
||||||
|
|
||||||
|
user.email = email
|
||||||
|
user.display_name = display_name
|
||||||
|
user.role = role
|
||||||
|
if password:
|
||||||
|
user.password_hash = hash_password(password)
|
||||||
|
if "disabled" in data:
|
||||||
|
user.disabled = bool(data["disabled"])
|
||||||
|
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
return {"ok": True}
|
||||||
|
|||||||
@@ -699,7 +699,7 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{me.role === "admin" && (
|
{me.role === "admin" && (
|
||||||
<AdminPanel open={adminOpen} onClose={() => setAdminOpen(false)} />
|
<AdminPanel open={adminOpen} onClose={() => setAdminOpen(false)} currentUserId={me.id} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<GamePickerCard
|
<GamePickerCard
|
||||||
|
|||||||
@@ -1,226 +1,167 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function AdminPanel({ open: dashboardOpen = false, onClose }) {
|
const emptyForm = { displayName: "", email: "", password: "", role: "user", disabled: false };
|
||||||
|
|
||||||
|
export default function AdminPanel({ open: dashboardOpen = false, onClose, currentUserId }) {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
const [userModalOpen, setUserModalOpen] = useState(false);
|
const [editingUser, setEditingUser] = useState(null);
|
||||||
const [displayName, setDisplayName] = useState("");
|
const [form, setForm] = useState(emptyForm);
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [role, setRole] = useState("user");
|
|
||||||
const [msg, setMsg] = useState("");
|
const [msg, setMsg] = useState("");
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dashboardOpen && !userModalOpen) return;
|
if (!dashboardOpen && !editorOpen) return;
|
||||||
|
const previous = document.body.style.overflow;
|
||||||
const prev = document.body.style.overflow;
|
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
|
return () => { document.body.style.overflow = previous; };
|
||||||
return () => {
|
}, [dashboardOpen, editorOpen]);
|
||||||
document.body.style.overflow = prev;
|
|
||||||
};
|
|
||||||
}, [dashboardOpen, userModalOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dashboardOpen) {
|
if (!dashboardOpen) {
|
||||||
setUserModalOpen(false);
|
setEditorOpen(false);
|
||||||
setMsg("");
|
setMsg("");
|
||||||
}
|
}
|
||||||
}, [dashboardOpen]);
|
}, [dashboardOpen]);
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => setUsers(await api("/admin/users"));
|
||||||
const u = await api("/admin/users");
|
|
||||||
setUsers(u);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { loadUsers().catch(() => {}); }, []);
|
||||||
loadUsers().catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const resetForm = () => {
|
const setField = (key, value) => setForm((current) => ({ ...current, [key]: value }));
|
||||||
setDisplayName("");
|
|
||||||
setEmail("");
|
|
||||||
setPassword("");
|
|
||||||
setRole("user");
|
|
||||||
};
|
|
||||||
|
|
||||||
const createUser = async () => {
|
const openCreate = () => {
|
||||||
|
setEditingUser(null);
|
||||||
|
setForm({ ...emptyForm });
|
||||||
setMsg("");
|
setMsg("");
|
||||||
|
setEditorOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (user) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
setForm({
|
||||||
|
displayName: user.display_name || "",
|
||||||
|
email: user.email || "",
|
||||||
|
password: "",
|
||||||
|
role: user.role || "user",
|
||||||
|
disabled: !!user.disabled,
|
||||||
|
});
|
||||||
|
setMsg("");
|
||||||
|
setEditorOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditor = () => {
|
||||||
|
setEditorOpen(false);
|
||||||
|
setEditingUser(null);
|
||||||
|
setMsg("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveUser = async () => {
|
||||||
|
setMsg("");
|
||||||
|
if (!form.email.trim()) return setMsg("❌ E-Mail ist erforderlich.");
|
||||||
|
if (!editingUser && form.password.length < 8) return setMsg("❌ Passwort muss mindestens 8 Zeichen haben.");
|
||||||
|
if (editingUser && form.password && form.password.length < 8) return setMsg("❌ Neues Passwort muss mindestens 8 Zeichen haben.");
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await api("/admin/users", {
|
const payload = {
|
||||||
method: "POST",
|
display_name: form.displayName,
|
||||||
body: JSON.stringify({ display_name: displayName, email, password, role }),
|
email: form.email,
|
||||||
|
role: form.role,
|
||||||
|
disabled: form.disabled,
|
||||||
|
};
|
||||||
|
if (form.password) payload.password = form.password;
|
||||||
|
|
||||||
|
await api(editingUser ? `/admin/users/${editingUser.id}` : "/admin/users", {
|
||||||
|
method: editingUser ? "PATCH" : "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
setMsg("✅ User erstellt.");
|
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
resetForm();
|
closeEditor();
|
||||||
setUserModalOpen(false);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMsg("❌ Fehler: " + (e?.message || "unknown"));
|
setMsg("❌ " + (e?.message || "Speichern fehlgeschlagen."));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUser = async (u) => {
|
const disableUser = async (user) => {
|
||||||
if (!window.confirm(`User wirklich löschen (deaktivieren)?\n\n${u.display_name || u.email}`)) return;
|
if (user.id === currentUserId) return;
|
||||||
|
if (!window.confirm(`User wirklich deaktivieren?\n\n${user.display_name || user.email}`)) return;
|
||||||
try {
|
try {
|
||||||
await api(`/admin/users/${u.id}`, { method: "DELETE" });
|
await api(`/admin/users/${user.id}`, { method: "DELETE" });
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Fehler: " + (e?.message || "unknown"));
|
alert("Fehler: " + (e?.message || "unknown"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setUserModalOpen(false);
|
|
||||||
setMsg("");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!dashboardOpen) return null;
|
if (!dashboardOpen) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div style={styles.modalOverlay} onMouseDown={onClose}>
|
<div style={styles.modalOverlay} onMouseDown={onClose}>
|
||||||
<div
|
<div style={{ ...styles.modalCard, width: "min(780px, 100%)", padding: 0 }} onMouseDown={(e) => e.stopPropagation()}>
|
||||||
style={{ ...styles.modalCard, width: "min(760px, 100%)", padding: 0 }}
|
<div style={{ padding: "18px 18px 16px" }}>
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
<div style={styles.modalHeader}>
|
||||||
>
|
<div>
|
||||||
<div style={{ ...styles.adminWrap, marginTop: 0, border: "none", boxShadow: "none", backdropFilter: "none", WebkitBackdropFilter: "none" }}>
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold, fontSize: 19 }}>Admin Dashboard</div>
|
||||||
<div style={styles.adminTop}>
|
<div style={{ marginTop: 3, color: stylesTokens.textDim, fontSize: 12 }}>Benutzer, Rollen und Zugangsdaten verwalten</div>
|
||||||
<div style={styles.adminTitle}>Admin Dashboard</div>
|
|
||||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
||||||
<button onClick={() => setUserModalOpen(true)} style={styles.primaryBtn}>
|
|
||||||
+ User anlegen
|
|
||||||
</button>
|
|
||||||
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Dashboard schließen">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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}
|
|
||||||
className="hp-admin-user-row"
|
|
||||||
style={{
|
|
||||||
...styles.userRow,
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="hp-admin-name" style={{ color: stylesTokens.textMain, fontWeight: 900 }}>
|
|
||||||
{u.display_name || "—"}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="hp-admin-email" style={{ color: stylesTokens.textDim, fontSize: 13 }}>{u.email}</div>
|
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Dashboard schließen">✕</button>
|
||||||
<div className="hp-admin-role" style={{ textAlign: "center", fontWeight: 900, color: stylesTokens.textGold }}>
|
|
||||||
{u.role}
|
|
||||||
</div>
|
|
||||||
<div className={`hp-admin-status ${u.disabled ? "hp-admin-status--disabled" : ""}`} style={{ textAlign: "center", opacity: 0.85, color: stylesTokens.textMain }}>
|
|
||||||
{u.disabled ? "disabled" : "active"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="hp-admin-action"
|
|
||||||
onClick={() => deleteUser(u)}
|
|
||||||
style={{
|
|
||||||
...styles.secondaryBtn,
|
|
||||||
padding: "8px 10px",
|
|
||||||
borderRadius: 12,
|
|
||||||
color: "#ffb3b3",
|
|
||||||
opacity: u.role === "admin" ? 0.4 : 1,
|
|
||||||
pointerEvents: u.role === "admin" ? "none" : "auto",
|
|
||||||
}}
|
|
||||||
title={u.role === "admin" ? "Admin kann nicht gelöscht werden" : "User löschen (deaktivieren)"}
|
|
||||||
>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{userModalOpen &&
|
<div style={{ marginTop: 18, display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10 }}>
|
||||||
createPortal(
|
<div style={{ color: stylesTokens.textMain, fontWeight: 900 }}>Vorhandene User <span style={{ color: stylesTokens.textDim, fontWeight: 700 }}>({users.length})</span></div>
|
||||||
<div style={styles.modalOverlay} onMouseDown={closeModal}>
|
<button onClick={openCreate} style={styles.primaryBtn}>+ User anlegen</button>
|
||||||
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
</div>
|
||||||
<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
|
<div style={{ marginTop: 10, display: "grid", gap: 9 }}>
|
||||||
style={{
|
{users.map((user) => (
|
||||||
marginTop: 12,
|
<div key={user.id} className="hp-admin-user-row" style={{ ...styles.userRow, alignItems: "center" }}>
|
||||||
display: "grid",
|
<div className="hp-admin-name" style={{ color: stylesTokens.textMain, fontWeight: 900 }}>{user.display_name || "—"}</div>
|
||||||
gap: 8,
|
<div className="hp-admin-email" style={{ color: stylesTokens.textDim, fontSize: 13 }}>{user.email}</div>
|
||||||
justifyItems: "center", // <<< zentriert alles
|
<div className="hp-admin-role" style={{ textAlign: "center", fontWeight: 900, color: stylesTokens.textGold }}>{user.role}</div>
|
||||||
}}
|
<div className={`hp-admin-status ${user.disabled ? "hp-admin-status--disabled" : ""}`} style={{ textAlign: "center", opacity: 0.85 }}>{user.disabled ? "disabled" : "active"}</div>
|
||||||
>
|
<div className="hp-admin-actions">
|
||||||
<input
|
<button className="hp-admin-action" onClick={() => openEdit(user)} style={{ ...styles.secondaryBtn, padding: "8px 11px" }}>Bearbeiten</button>
|
||||||
value={displayName}
|
<button className="hp-admin-action" onClick={() => disableUser(user)} disabled={user.id === currentUserId || user.disabled} style={{ ...styles.secondaryBtn, padding: "8px 11px", color: "#ffb3b3" }}>
|
||||||
onChange={(e) => setDisplayName(e.target.value)}
|
{user.disabled ? "Deaktiviert" : "Deaktivieren"}
|
||||||
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 }}>
|
|
||||||
Tipp: Name wird in TopBar & Siegeranzeige genutzt.
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>,
|
</div>
|
||||||
document.body
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{editorOpen && createPortal(
|
||||||
|
<div style={styles.modalOverlay} onMouseDown={closeEditor}>
|
||||||
|
<div style={{ ...styles.modalCard, width: "min(470px, 100%)" }} onMouseDown={(e) => e.stopPropagation()}>
|
||||||
|
<div style={styles.modalHeader}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold, fontSize: 18 }}>{editingUser ? "User bearbeiten" : "Neuen User anlegen"}</div>
|
||||||
|
<div style={{ marginTop: 3, color: stylesTokens.textDim, fontSize: 12 }}>{editingUser ? "Profil und Zugangsdaten aktualisieren" : "Ein neues Benutzerkonto erstellen"}</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={closeEditor} style={styles.modalCloseBtn} aria-label="Schließen">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 18, display: "grid", gap: 10 }}>
|
||||||
|
<label style={styles.adminFieldLabel}>Anzeigename<input value={form.displayName} onChange={(e) => setField("displayName", e.target.value)} placeholder="z. B. Sascha Nesterovic" style={styles.input} autoFocus /></label>
|
||||||
|
<label style={styles.adminFieldLabel}>E-Mail<input value={form.email} onChange={(e) => setField("email", e.target.value)} placeholder="name@example.com" style={styles.input} inputMode="email" /></label>
|
||||||
|
<label style={styles.adminFieldLabel}>{editingUser ? "Neues Passwort (optional)" : "Passwort"}<input value={form.password} onChange={(e) => setField("password", e.target.value)} placeholder={editingUser ? "Leer lassen = unverändert" : "Mindestens 8 Zeichen"} type="password" style={styles.input} /></label>
|
||||||
|
<label style={styles.adminFieldLabel}>Rolle<select value={form.role} onChange={(e) => setField("role", e.target.value)} disabled={editingUser?.id === currentUserId} style={styles.input}><option value="user">User</option><option value="admin">Admin</option></select></label>
|
||||||
|
{editingUser && <label className="hp-admin-check"><input type="checkbox" checked={!form.disabled} onChange={(e) => setField("disabled", !e.target.checked)} /> Konto ist aktiv</label>}
|
||||||
|
{msg && <div style={{ color: "#ffb3b3", fontSize: 13 }}>{msg}</div>}
|
||||||
|
<button onClick={saveUser} style={{ ...styles.primaryBtn, width: "100%", marginTop: 4 }} disabled={saving}>{saving ? "Speichern …" : editingUser ? "Änderungen speichern" : "User erstellen"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function HelpModal({ open, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={styles.helpListRow}>
|
<div style={styles.helpListRow}>
|
||||||
<span style={styles.helpMiniTag}>🛡️</span>
|
<span style={styles.helpMiniTag}>🛡️</span>
|
||||||
<div><b>Admin Dashboard</b> = nur für Administratoren</div>
|
<div><b>Admin Dashboard</b> = User anlegen, bearbeiten, Rollen ändern, Passwörter setzen und Konten deaktivieren</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -105,8 +105,11 @@ export function useHpGlobalStyles() {
|
|||||||
@keyframes hpStartLine { 0%, 100% { opacity: .3; transform: scaleX(.7); } 50% { opacity: 1; transform: scaleX(1); } }
|
@keyframes hpStartLine { 0%, 100% { opacity: .3; transform: scaleX(.7); } 50% { opacity: 1; transform: scaleX(1); } }
|
||||||
.hp-topbar-actions { margin-left: auto; }
|
.hp-topbar-actions { margin-left: auto; }
|
||||||
.hp-user-menu-wrap { min-width: 0; }
|
.hp-user-menu-wrap { min-width: 0; }
|
||||||
.hp-admin-user-row { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.5fr) 70px 76px 86px; }
|
.hp-admin-user-row { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.5fr) 70px 76px 150px; }
|
||||||
.hp-admin-action { min-width: 0; }
|
.hp-admin-action { min-width: 0; }
|
||||||
|
.hp-admin-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; }
|
||||||
|
.hp-admin-check { display: flex; align-items: center; gap: 8px; color: var(--hp-textDim); font-size: 13px; }
|
||||||
|
.hp-admin-check input { width: 18px; height: 18px; accent-color: var(--hp-textGold); }
|
||||||
.hp-admin-role { padding: 4px 8px; border-radius: 999px; background: rgba(233,216,166,0.10); border: 1px solid rgba(233,216,166,0.16); font-size: 12px; }
|
.hp-admin-role { padding: 4px 8px; border-radius: 999px; background: rgba(233,216,166,0.10); border: 1px solid rgba(233,216,166,0.16); font-size: 12px; }
|
||||||
.hp-admin-status { color: #baf3c9 !important; font-size: 13px; }
|
.hp-admin-status { color: #baf3c9 !important; font-size: 13px; }
|
||||||
.hp-admin-status--disabled { color: #ffb3b3 !important; }
|
.hp-admin-status--disabled { color: #ffb3b3 !important; }
|
||||||
@@ -126,7 +129,8 @@ export function useHpGlobalStyles() {
|
|||||||
.hp-admin-email { grid-column: 1; grid-row: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px !important; }
|
.hp-admin-email { grid-column: 1; grid-row: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px !important; }
|
||||||
.hp-admin-role { grid-column: 2; grid-row: 1; }
|
.hp-admin-role { grid-column: 2; grid-row: 1; }
|
||||||
.hp-admin-status { grid-column: 2; grid-row: 2; }
|
.hp-admin-status { grid-column: 2; grid-row: 2; }
|
||||||
.hp-admin-action { grid-column: 2; grid-row: 3; justify-self: end; width: auto; min-width: 94px; min-height: 38px; padding: 7px 14px !important; }
|
.hp-admin-actions { grid-column: 1 / -1; grid-row: 3; width: 100%; justify-content: flex-end; }
|
||||||
|
.hp-admin-actions .hp-admin-action { width: auto; min-width: 0; min-height: 38px; padding: 7px 10px !important; }
|
||||||
.hp-row { grid-template-columns: minmax(0, 1fr) 42px 62px !important; gap: 7px !important; padding: 12px 11px !important; }
|
.hp-row { grid-template-columns: minmax(0, 1fr) 42px 62px !important; gap: 7px !important; padding: 12px 11px !important; }
|
||||||
.hp-row button { min-height: 40px; }
|
.hp-row button { min-height: 40px; }
|
||||||
button { min-height: 42px; }
|
button { min-height: 42px; }
|
||||||
|
|||||||
@@ -180,6 +180,13 @@ export const styles = {
|
|||||||
fontWeight: 1000,
|
fontWeight: 1000,
|
||||||
color: stylesTokens.textGold,
|
color: stylesTokens.textGold,
|
||||||
},
|
},
|
||||||
|
adminFieldLabel: {
|
||||||
|
display: "grid",
|
||||||
|
gap: 5,
|
||||||
|
color: stylesTokens.textDim,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 800,
|
||||||
|
},
|
||||||
userRow: {
|
userRow: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 80px 90px",
|
gridTemplateColumns: "1fr 80px 90px",
|
||||||
|
|||||||
Reference in New Issue
Block a user