Add display_name support for users in backend and frontend

This commit introduces the `display_name` field to the user model. It updates database migrations, API endpoints, and the admin panel to handle this field. Additionally, the `display_name` is now shown in the TopBar and WinnerBadge components, improving user experience.
This commit is contained in:
2026-02-06 12:09:21 +01:00
parent 4a012b7345
commit 3a66c0cf74
7 changed files with 140 additions and 26 deletions

View File

@@ -7,6 +7,7 @@ export default function AdminPanel() {
const [users, setUsers] = useState([]);
const [open, setOpen] = useState(false);
const [displayName, setDisplayName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState("user");
@@ -22,6 +23,7 @@ export default function AdminPanel() {
}, []);
const resetForm = () => {
setDisplayName("");
setEmail("");
setPassword("");
setRole("user");
@@ -32,7 +34,7 @@ export default function AdminPanel() {
try {
await api("/admin/users", {
method: "POST",
body: JSON.stringify({ email, password, role }),
body: JSON.stringify({ display_name: displayName, email, password, role }),
});
setMsg("✅ User erstellt.");
await loadUsers();
@@ -43,6 +45,16 @@ export default function AdminPanel() {
}
};
const deleteUser = async (u) => {
if (!window.confirm(`User wirklich löschen (deaktivieren)?\n\n${u.display_name || u.email}`)) return;
try {
await api(`/admin/users/${u.id}`, { method: "DELETE" });
await loadUsers();
} catch (e) {
alert("Fehler: " + (e?.message || "unknown"));
}
};
const closeModal = () => {
setOpen(false);
setMsg("");
@@ -63,14 +75,39 @@ export default function AdminPanel() {
<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
key={u.id}
style={{
...styles.userRow,
gridTemplateColumns: "1fr 1fr 80px 90px 92px",
alignItems: "center",
}}
>
<div style={{ color: stylesTokens.textMain, fontWeight: 900 }}>
{u.display_name || "—"}
</div>
<div style={{ color: stylesTokens.textDim, fontSize: 13 }}>{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>
<button
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>
@@ -88,13 +125,21 @@ export default function AdminPanel() {
</div>
<div style={{ marginTop: 12, display: "grid", gap: 10 }}>
<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}
autoFocus
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
@@ -102,6 +147,7 @@ export default function AdminPanel() {
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>
@@ -125,7 +171,7 @@ export default function AdminPanel() {
</div>
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
Tipp: Klick auf Item: Grün Rot Grau Leer
Tipp: Name wird in TopBar & Siegeranzeige genutzt.
</div>
</div>
</div>
@@ -133,4 +179,4 @@ export default function AdminPanel() {
)}
</div>
);
}
}

View File

@@ -2,6 +2,8 @@ import React from "react";
import { styles } from "../styles/styles";
import { stylesTokens } from "../styles/theme";
const displayName = (me?.display_name || "").trim() || me.email;
export default function TopBar({
me,
userMenuOpen,
@@ -18,7 +20,7 @@ export default function TopBar({
Notizbogen
</div>
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
{me.email}
{displayName}
</div>
</div>

View File

@@ -1,8 +1,26 @@
import React from "react";
import { stylesTokens } from "../styles/theme";
export default function WinnerBadge({ winnerEmail }) {
if (!winnerEmail) return null;
/**
* Props:
* - winner: { display_name?: string, email?: string } | null
* (oder als Fallback:)
* - winnerEmail: string | null
*/
export default function WinnerBadge({ winner, winnerEmail }) {
const name =
(winner?.display_name || "").trim() ||
(winner?.email || "").trim() ||
(winnerEmail || "").trim();
if (!name) return null;
// Optional: wenn display_name vorhanden ist, Email klein anzeigen
const showEmail =
winner &&
(winner?.email || "").trim() &&
(winner?.display_name || "").trim() &&
winner.email.trim().toLowerCase() !== winner.display_name.trim().toLowerCase();
return (
<div
@@ -22,9 +40,18 @@ export default function WinnerBadge({ winnerEmail }) {
>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ fontSize: 18 }}>🏆</div>
<div style={{ color: stylesTokens.textMain, fontWeight: 900 }}>
Sieger:
<span style={{ color: stylesTokens.textGold }}>{" "}{winnerEmail}</span>
<div style={{ display: "grid", gap: 2 }}>
<div style={{ color: stylesTokens.textMain, fontWeight: 900 }}>
Sieger:
<span style={{ color: stylesTokens.textGold }}>{" "}{name}</span>
</div>
{showEmail && (
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>
{winner.email}
</div>
)}
</div>
</div>