Compare commits

..

12 Commits

Author SHA1 Message Date
62eb7e6e58 Update user menu display for cleaner interface
Replaced the email with a generic "Account" label in the user menu for better design consistency and to avoid truncation issues. Adjusted the dropdown max width to 180px for improved alignment with the new layout.
2026-02-03 20:27:21 +01:00
7036f29481 Add password change functionality to user settings
Implemented a secure password change endpoint in the backend with validation. Enhanced the frontend to include a modal for updating the user's password, ensuring real-time input validation and user feedback.
2026-02-03 20:24:20 +01:00
bf37850e79 Improve chip selection and modal closing behavior
Updated the chip handling logic to ensure a smoother user experience by immediately closing modals in the frontend before performing asynchronous operations. Enhanced error handling and streamlined tag display logic for clarity and consistency.
2026-02-03 20:15:46 +01:00
a9dbbd65a4 Update status logic to include "m" in note_tag checks
Previously, only "i" and "s" were considered in the note_tag check for determining effectiveStatus. This update adds "m" to the condition, ensuring accurate status handling for entries with note_tag "m".
2026-02-03 20:09:45 +01:00
ca15266fb9 Add local storage handling for chip selection in frontend
Introduced functions to manage chip data in local storage, ensuring smoother handling of chip selections without backend dependency. Adjusted UI logic to display chip tags dynamically and modified "s" state handling to integrate local storage data seamlessly.
2026-02-03 18:46:33 +01:00
e2b97b69e9 Refactor tag handling and remove unused functions
Consolidated tag cycling logic into a single function, simplifying and clarifying its purpose. Removed redundant functions related to tag parsing and storage. Updated UI elements for better readability and consistency.
2026-02-03 16:10:32 +01:00
59f477c343 Rename CHIP_LIST to CHIP_OPTIONS for clarity
The constant name was updated to better reflect its purpose as a list of selectable options. This improves code readability and maintainability.
2026-02-03 15:53:40 +01:00
7be21969e7 Add cycleTag function for tag value transitions
Implemented the cycleTag function to handle transitions between tag states ("i", "m", "s") and reset for special cases starting with "s.". This prepares the application to support dynamic tag cycling functionality.
2026-02-03 15:49:09 +01:00
db35a7b0c9 Refactor chip state variable names for consistency
Renamed state variables `chipPickOpen` and `chipPickEntry` to `chipOpen` and `chipEntry`. This change improves naming consistency across the component, simplifying readability and maintenance.
2026-02-03 15:39:10 +01:00
d2e2286627 Refactor chip selection logic and UI improvements
Unified chip modal functionality by consolidating state variables and methods. Enhanced UI consistency for effective statuses and streamlined code for readability and maintainability. Updated styles for better visual appeal and clarity.
2026-02-03 15:35:55 +01:00
b8fc47e881 Update text style for additional conditions
Adjusted text decoration and opacity to apply when certain tags ("i" or "s") are set. This ensures better visual feedback for the specified conditions in the application interface.
2026-02-03 15:22:18 +01:00
2ec7c63119 Refactor tag handling and add chip localStorage support
Refactored tag handling logic by introducing helper functions to improve clarity and maintainability. Added localStorage support for storing and retrieving chip values associated with entries, ensuring smoother transitions and proper state management across sessions. Simplified backend interactions for the "s" tag and improved display logic for tags with chips.
2026-02-03 15:13:21 +01:00
2 changed files with 397 additions and 102 deletions

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..db import get_db from ..db import get_db
from ..models import User from ..models import User
from ..security import verify_password, make_session_value, set_session, clear_session, get_session_user_id from ..security import verify_password, make_session_value, set_session, clear_session, get_session_user_id, hash_password
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@@ -31,3 +31,22 @@ def me(req: Request, db: Session = Depends(get_db)):
raise HTTPException(status_code=401, detail="not logged in") raise HTTPException(status_code=401, detail="not logged in")
return {"id": user.id, "email": user.email, "role": user.role} return {"id": user.id, "email": user.email, "role": user.role}
@router.patch("/password")
def set_password(data: dict, req: Request, db: Session = Depends(get_db)):
uid = get_session_user_id(req)
if not uid:
raise HTTPException(status_code=401, detail="not logged in")
password = data.get("password") or ""
if len(password) < 8:
raise HTTPException(status_code=400, detail="password too short (min 8)")
user = db.query(User).filter(User.id == uid, User.disabled == False).first()
if not user:
raise HTTPException(status_code=401, detail="not logged in")
user.password_hash = hash_password(password)
db.add(user)
db.commit()
return {"ok": True}

View File

@@ -13,25 +13,44 @@ async function api(path, opts = {}) {
return res.json(); return res.json();
} }
function parseTag(tag) { /**
// erlaubt: null | "i" | "m" | "s" | "s.AL" * Backend erlaubt: null | "i" | "m" | "s"
if (!tag) return { base: null, chip: null }; * Rotation:
if (tag === "i") return { base: "i", chip: null }; * null -> i -> m -> s (Popup) -> null
if (tag === "m") return { base: "m", chip: null }; */
if (tag === "s") return { base: "s", chip: null }; function cycleTag(tag) {
if (tag.startsWith("s.")) return { base: "s", chip: tag.slice(2) || null }; if (!tag) return "i";
return { base: tag, chip: null }; if (tag === "i") return "m";
if (tag === "m") return "s";
return null; // "s" -> null
} }
function nextBaseTag(tag) { /* ========= Chip localStorage (Frontend-only) ========= */
const { base } = parseTag(tag); function chipStorageKey(gameId, entryId) {
if (!base) return "i"; return `chip:${gameId}:${entryId}`;
if (base === "i") return "m"; }
if (base === "m") return "s"; // hier öffnen wir dann das Popup
// wenn s (egal ob s oder s.XY), dann zurück auf leer function getChipLS(gameId, entryId) {
try {
return localStorage.getItem(chipStorageKey(gameId, entryId));
} catch {
return null; return null;
}
} }
function setChipLS(gameId, entryId, chip) {
try {
localStorage.setItem(chipStorageKey(gameId, entryId), chip);
} catch {}
}
function clearChipLS(gameId, entryId) {
try {
localStorage.removeItem(chipStorageKey(gameId, entryId));
} catch {}
}
/* ========= Admin Panel ========= */
function AdminPanel() { function AdminPanel() {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
@@ -164,6 +183,7 @@ function AdminPanel() {
); );
} }
/* ========= App ========= */
export default function App() { export default function App() {
const [me, setMe] = useState(null); const [me, setMe] = useState(null);
const [loginEmail, setLoginEmail] = useState(""); const [loginEmail, setLoginEmail] = useState("");
@@ -175,11 +195,21 @@ export default function App() {
const [sheet, setSheet] = useState(null); const [sheet, setSheet] = useState(null);
const [pulseId, setPulseId] = useState(null); const [pulseId, setPulseId] = useState(null);
const [chipPickOpen, setChipPickOpen] = useState(false); // Chip popup
const [chipPickEntry, setChipPickEntry] = useState(null); const [chipOpen, setChipOpen] = useState(false);
const [chipEntry, setChipEntry] = useState(null);
const [helpOpen, setHelpOpen] = useState(false); const [helpOpen, setHelpOpen] = useState(false);
// User dropdown + Passwort Modal
const [userMenuOpen, setUserMenuOpen] = useState(false);
const [pwOpen, setPwOpen] = useState(false);
const [pw1, setPw1] = useState("");
const [pw2, setPw2] = useState("");
const [pwMsg, setPwMsg] = useState("");
const [pwSaving, setPwSaving] = useState(false);
const load = async () => { const load = async () => {
const m = await api("/auth/me"); const m = await api("/auth/me");
setMe(m); setMe(m);
@@ -190,6 +220,18 @@ export default function App() {
if (gs[0] && !gameId) setGameId(gs[0].id); if (gs[0] && !gameId) setGameId(gs[0].id);
}; };
// Usermanager
useEffect(() => {
const onDown = (e) => {
// wenn Dropdown offen & Klick ist NICHT in einem Element mit data-user-menu
const root = e.target?.closest?.("[data-user-menu]");
if (!root) setUserMenuOpen(false);
};
if (userMenuOpen) document.addEventListener("mousedown", onDown);
return () => document.removeEventListener("mousedown", onDown);
}, [userMenuOpen]);
// Google Fonts // Google Fonts
useEffect(() => { useEffect(() => {
if (document.getElementById("hp-fonts")) return; if (document.getElementById("hp-fonts")) return;
@@ -225,7 +267,6 @@ export default function App() {
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes popIn { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes rowPulse { 0%{ transform: scale(1); } 50%{ transform: scale(1.01); } 100%{ transform: scale(1); } } @keyframes rowPulse { 0%{ transform: scale(1); } 50%{ transform: scale(1.01); } 100%{ transform: scale(1); } }
@keyframes candleGlow { @keyframes candleGlow {
0% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); } 0% { opacity: .55; transform: translateY(0px) scale(1); filter: blur(16px); }
35% { opacity: .85; transform: translateY(-2px) scale(1.02); filter: blur(18px); } 35% { opacity: .85; transform: translateY(-2px) scale(1.02); filter: blur(18px); }
@@ -246,7 +287,7 @@ export default function App() {
document.body.style.padding = "0"; document.body.style.padding = "0";
}, []); }, []);
// Global CSS: Dark + Gold base // Global CSS
useEffect(() => { useEffect(() => {
if (document.getElementById("hp-global-style")) return; if (document.getElementById("hp-global-style")) return;
const style = document.createElement("style"); const style = document.createElement("style");
@@ -261,15 +302,11 @@ export default function App() {
background: ${stylesTokens.pageBg}; background: ${stylesTokens.pageBg};
color: ${stylesTokens.textMain}; color: ${stylesTokens.textMain};
} }
body { body {
overflow-x: hidden; overflow-x: hidden;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
#root { background: transparent; } #root { background: transparent; }
/* Safari/Chrome tap highlight reduzieren (kein hover/flash) */
* { -webkit-tap-highlight-color: transparent; } * { -webkit-tap-highlight-color: transparent; }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
@@ -314,6 +351,52 @@ export default function App() {
setSheet(null); setSheet(null);
}; };
// ===== Password change =====
const openPwModal = () => {
setPwMsg("");
setPw1("");
setPw2("");
setPwOpen(true);
setUserMenuOpen(false);
};
const closePwModal = () => {
setPwOpen(false);
setPwMsg("");
setPw1("");
setPw2("");
};
const savePassword = async () => {
setPwMsg("");
if (!pw1 || pw1.length < 8) {
setPwMsg("❌ Passwort muss mindestens 8 Zeichen haben.");
return;
}
if (pw1 !== pw2) {
setPwMsg("❌ Passwörter stimmen nicht überein.");
return;
}
setPwSaving(true);
try {
await api("/auth/password", {
method: "PATCH",
body: JSON.stringify({ password: pw1 }),
});
setPwMsg("✅ Passwort gespeichert.");
// Optional: nach kurzer Zeit automatisch schließen
setTimeout(() => closePwModal(), 650);
} catch (e) {
// dein api() wirft res.text() -> oft JSON Detail. Wir zeigen es einfach roh.
setPwMsg("❌ Fehler: " + (e?.message || "unknown"));
} finally {
setPwSaving(false);
}
};
const newGame = async () => { const newGame = async () => {
const g = await api("/games", { const g = await api("/games", {
method: "POST", method: "POST",
@@ -347,40 +430,93 @@ export default function App() {
setTimeout(() => setPulseId(null), 220); setTimeout(() => setPulseId(null), 220);
}; };
// Notiz-Button: i -> m -> (Popup) s -> null
const toggleTag = async (entry) => { const toggleTag = async (entry) => {
const next = nextBaseTag(entry.note_tag); const next = cycleTag(entry.note_tag);
// Wenn wir bei "s" angekommen sind -> Popup öffnen statt sofort setzen // Wenn wir zu "s" gehen würden -> Chip Popup öffnen, aber NICHT ins Backend schreiben
if (next === "s") { if (next === "s") {
setChipPickEntry(entry); setChipEntry(entry);
setChipPickOpen(true); setChipOpen(true);
return; return;
} }
// normal setzen (— / i / m) // Wenn wir auf null gehen, Chip lokal löschen (weil s -> —)
if (next === null) {
clearChipLS(gameId, entry.entry_id);
}
await api(`/games/${gameId}/sheet/${entry.entry_id}`, { await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ note_tag: next }), body: JSON.stringify({ note_tag: next }),
}); });
await reloadSheet(); await reloadSheet();
}; };
const closeChipPick = () => { // Chip wählen:
setChipPickOpen(false); // Backend: note_tag = "s"
setChipPickEntry(null); // Frontend: Chip in localStorage
};
const chooseChip = async (chip) => { const chooseChip = async (chip) => {
if (!chipPickEntry) return; if (!chipEntry) return;
await api(`/games/${gameId}/sheet/${chipPickEntry.entry_id}`, { // UI sofort schließen -> fühlt sich besser an
const entry = chipEntry;
setChipOpen(false);
setChipEntry(null);
// local speichern
setChipLS(gameId, entry.entry_id, chip);
try {
// Backend bekommt nur "s"
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ note_tag: `s.${chip}` }), body: JSON.stringify({ note_tag: "s" }),
}); });
} finally {
closeChipPick();
await reloadSheet(); await reloadSheet();
}
};
// X im Modal:
// Backend zurück auf null und lokalen Chip löschen
const closeChipModalToDash = async () => {
if (!chipEntry) {
setChipOpen(false);
return;
}
// UI sofort schließen
const entry = chipEntry;
setChipOpen(false);
setChipEntry(null);
// Frontend-only Chip entfernen
clearChipLS(gameId, entry.entry_id);
try {
// Backend zurück auf —
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: null }),
});
} finally {
await reloadSheet();
}
};
// Anzeige im Tag-Button:
// - "s" wird zu "s.AL" (aus localStorage), sonst "s"
const displayTag = (entry) => {
const t = entry.note_tag;
if (!t) return "—";
if (t === "s") {
const chip = getChipLS(gameId, entry.entry_id);
return chip ? `s.${chip}` : "s"; // <-- genau wie gewünscht
}
return t; // i oder m
}; };
// --- helpers --- // --- helpers ---
@@ -408,7 +544,8 @@ export default function App() {
const getStatusBadge = (status) => { const getStatusBadge = (status) => {
if (status === 2) return { color: "#baf3c9", background: "rgba(0,190,80,0.18)" }; if (status === 2) return { color: "#baf3c9", background: "rgba(0,190,80,0.18)" };
if (status === 1) return { color: "#ffb3b3", background: "rgba(255,35,35,0.18)" }; if (status === 1) return { color: "#ffb3b3", background: "rgba(255,35,35,0.18)" };
if (status === 3) return { color: "rgba(233,216,166,0.85)", background: "rgba(140,140,140,0.14)" }; if (status === 3)
return { color: "rgba(233,216,166,0.85)", background: "rgba(140,140,140,0.14)" };
return { color: "rgba(233,216,166,0.75)", background: "rgba(255,255,255,0.08)" }; return { color: "rgba(233,216,166,0.75)", background: "rgba(255,255,255,0.08)" };
}; };
@@ -427,9 +564,7 @@ export default function App() {
<div style={styles.loginCard}> <div style={styles.loginCard}>
<div style={styles.loginTitle}>Zauber-Detektiv Notizbogen</div> <div style={styles.loginTitle}>Zauber-Detektiv Notizbogen</div>
<div style={styles.loginSubtitle}> <div style={styles.loginSubtitle}>Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen</div>
Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen
</div>
<div style={{ marginTop: 18, display: "grid", gap: 12 }}> <div style={{ marginTop: 18, display: "grid", gap: 12 }}>
<div style={styles.loginFieldWrap}> <div style={styles.loginFieldWrap}>
@@ -497,15 +632,42 @@ export default function App() {
<div style={styles.topBar}> <div style={styles.topBar}>
<div> <div>
<div style={{ fontWeight: 900, color: stylesTokens.textGold }}>{me.email}</div> <div style={{ fontWeight: 900, color: stylesTokens.textGold }}>{me.email}</div>
<div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}> <div style={{ fontSize: 12, opacity: 0.8, color: stylesTokens.textDim }}>{me.role}</div>
{me.role}
</div>
</div> </div>
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8, alignItems: "center" }} data-user-menu>
<button onClick={doLogout} style={styles.secondaryBtn}> <div style={{ position: "relative" }}>
<button
onClick={() => setUserMenuOpen((v) => !v)}
style={styles.userBtn}
title="User Menü"
>
<span style={{ fontSize: 16, lineHeight: 1 }}>👤</span>
<span>Account</span>
<span style={{ opacity: 0.8 }}></span>
</button>
{userMenuOpen && (
<div style={styles.userDropdown}>
<button onClick={openPwModal} style={styles.userDropdownItem}>
Passwort setzen
</button>
<div style={styles.userDropdownDivider} />
<button
onClick={() => {
setUserMenuOpen(false);
doLogout();
}}
style={{ ...styles.userDropdownItem, color: "#ffb3b3" }}
>
Logout Logout
</button> </button>
</div>
)}
</div>
<button onClick={newGame} style={styles.primaryBtn}> <button onClick={newGame} style={styles.primaryBtn}>
+ Neues Spiel + Neues Spiel
</button> </button>
@@ -558,25 +720,45 @@ export default function App() {
<span style={{ ...styles.helpBadge, background: "rgba(0,190,80,0.18)", color: "#baf3c9" }}> <span style={{ ...styles.helpBadge, background: "rgba(0,190,80,0.18)", color: "#baf3c9" }}>
</span> </span>
<div><b>Grün</b> = bestätigt / fix richtig</div> <div>
<b>Grün</b> = bestätigt / fix richtig
</div>
</div> </div>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={{ ...styles.helpBadge, background: "rgba(255,35,35,0.18)", color: "#ffb3b3" }}> <span style={{ ...styles.helpBadge, background: "rgba(255,35,35,0.18)", color: "#ffb3b3" }}>
</span> </span>
<div><b>Rot</b> = ausgeschlossen / fix falsch</div> <div>
<b>Rot</b> = ausgeschlossen / fix falsch
</div>
</div> </div>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={{ ...styles.helpBadge, background: "rgba(140,140,140,0.14)", color: "rgba(233,216,166,0.85)" }}> <span
style={{
...styles.helpBadge,
background: "rgba(140,140,140,0.14)",
color: "rgba(233,216,166,0.85)",
}}
>
? ?
</span> </span>
<div><b>Grau</b> = unsicher / vielleicht</div> <div>
<b>Grau</b> = unsicher / vielleicht
</div>
</div> </div>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={{ ...styles.helpBadge, background: "rgba(255,255,255,0.08)", color: "rgba(233,216,166,0.75)" }}> <span
style={{
...styles.helpBadge,
background: "rgba(255,255,255,0.08)",
color: "rgba(233,216,166,0.75)",
}}
>
</span> </span>
<div><b>Leer</b> = unknown / noch nicht bewertet</div> <div>
<b>Leer</b> = unknown / noch nicht bewertet
</div>
</div> </div>
</div> </div>
@@ -590,73 +772,115 @@ export default function App() {
<div style={styles.helpList}> <div style={styles.helpList}>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={styles.helpMiniTag}>i</span> <span style={styles.helpMiniTag}>i</span>
<div><b>i</b> = Ich habe diese Geheimkarte</div> <div>
<b>i</b> = Ich habe diese Geheimkarte
</div>
</div> </div>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={styles.helpMiniTag}>m</span> <span style={styles.helpMiniTag}>m</span>
<div><b>m</b> = Geheimkarte aus dem mittleren Deck</div> <div>
<b>m</b> = Geheimkarte aus dem mittleren Deck
</div>
</div> </div>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={styles.helpMiniTag}>s</span> <span style={styles.helpMiniTag}>s</span>
<div><b>s</b> = Ein anderer Spieler hat diese Karte</div> <div>
<b>s</b> = Ein anderer Spieler hat diese Karte (Chip Auswahl)
</div>
</div> </div>
<div style={styles.helpListRow}> <div style={styles.helpListRow}>
<span style={styles.helpMiniTag}></span> <span style={styles.helpMiniTag}></span>
<div><b></b> = keine Notiz</div> <div>
<b></b> = keine Notiz
</div>
</div> </div>
</div> </div>
<div style={styles.helpDivider} /> <div style={styles.helpDivider} />
<div style={styles.helpText}> <div style={styles.helpText}>
Tipp: Jeder Spieler sieht nur seine eigenen Notizen andere Spieler können nicht in deinen Zettel schauen. Tipp: Jeder Spieler sieht nur seine eigenen Notizen andere Spieler können nicht in deinen
Zettel schauen.
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{chipPickOpen && ( {pwOpen && (
<div style={styles.modalOverlay} onMouseDown={closeChipPick}> <div style={styles.modalOverlay} onMouseDown={closePwModal}>
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}> <div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
<div style={styles.modalHeader}> <div style={styles.modalHeader}>
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}> <div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Passwort setzen</div>
Wer hat die Karte? <button onClick={closePwModal} style={styles.modalCloseBtn} aria-label="Schließen">
</div>
<button
onClick={closeChipPick}
style={styles.modalCloseBtn}
aria-label="Schließen"
>
</button> </button>
</div> </div>
<div style={{ marginTop: 12, color: stylesTokens.textMain, opacity: 0.9 }}> <div style={{ marginTop: 12, display: "grid", gap: 10 }}>
Chip auswählen: <input
value={pw1}
onChange={(e) => setPw1(e.target.value)}
placeholder="Neues Passwort"
type="password"
style={styles.input}
autoFocus
/>
<input
value={pw2}
onChange={(e) => setPw2(e.target.value)}
placeholder="Neues Passwort wiederholen"
type="password"
style={styles.input}
/>
{pwMsg && <div style={{ opacity: 0.92, color: stylesTokens.textMain }}>{pwMsg}</div>}
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
<button onClick={closePwModal} style={styles.secondaryBtn} disabled={pwSaving}>
Abbrechen
</button>
<button onClick={savePassword} style={styles.primaryBtn} disabled={pwSaving}>
{pwSaving ? "Speichern..." : "Speichern"}
</button>
</div> </div>
<div style={{ fontSize: 12, opacity: 0.75, color: stylesTokens.textDim }}>
Hinweis: Mindestens 8 Zeichen empfohlen.
</div>
</div>
</div>
</div>
)}
{/* Chip Popup */}
{chipOpen && (
<div style={styles.modalOverlay} onMouseDown={closeChipModalToDash}>
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
<div style={styles.modalHeader}>
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Wer hat die Karte?</div>
<button onClick={closeChipModalToDash} style={styles.modalCloseBtn} aria-label="Schließen">
</button>
</div>
<div style={{ marginTop: 12, color: stylesTokens.textMain }}>Chip auswählen:</div>
<div style={styles.chipGrid}> <div style={styles.chipGrid}>
{CHIP_LIST.map((c) => ( {CHIP_LIST.map((c) => (
<button <button key={c} onClick={() => chooseChip(c)} style={styles.chipBtn}>
key={c}
onClick={() => chooseChip(c)}
style={styles.chipBtn}
title={`Setze s.${c}`}
>
{c} {c}
</button> </button>
))} ))}
</div> </div>
<div style={{ marginTop: 10, fontSize: 12, color: stylesTokens.textDim }}> <div style={{ marginTop: 12, fontSize: 12, color: stylesTokens.textDim }}>
Tipp: Wenn du wieder auf den Notiz-Button klickst, gehts von <b>s.XX</b> zurück auf <b></b>. Tipp: Wenn du wieder auf den Notiz-Button klickst, gehts von <b>s.XX</b> zurück auf .
</div> </div>
</div> </div>
</div> </div>
)} )}
<div style={{ marginTop: 14, display: "grid", gap: 14 }}> <div style={{ marginTop: 14, display: "grid", gap: 14 }}>
{sections.map((sec) => ( {sections.map((sec) => (
<div key={sec.key} style={styles.card}> <div key={sec.key} style={styles.card}>
@@ -664,29 +888,37 @@ export default function App() {
<div style={{ display: "grid" }}> <div style={{ display: "grid" }}>
{sec.entries.map((e) => { {sec.entries.map((e) => {
const badge = getStatusBadge(e.status); // UI "rot" wenn note_tag i oder s (Backend s wird als s.XX angezeigt)
const isIorMorS = e.note_tag === "i" || e.note_tag === "m" || e.note_tag === "s";
const effectiveStatus = e.status === 0 && isIorMorS ? 1 : e.status;
const badge = getStatusBadge(effectiveStatus);
return ( return (
<div <div
key={e.entry_id} key={e.entry_id}
className="hp-row" className="hp-row"
style={{ style={{
...styles.row, ...styles.row,
background: getRowBg(e.status), background: getRowBg(effectiveStatus),
animation: pulseId === e.entry_id ? "rowPulse 220ms ease-out" : "none", animation: pulseId === e.entry_id ? "rowPulse 220ms ease-out" : "none",
borderLeft: borderLeft:
e.status === 2 ? "4px solid rgba(0,190,80,0.55)" : effectiveStatus === 2
e.status === 1 ? "4px solid rgba(255,35,35,0.55)" : ? "4px solid rgba(0,190,80,0.55)"
e.status === 3 ? "4px solid rgba(233,216,166,0.22)" : : effectiveStatus === 1
"4px solid rgba(0,0,0,0)", ? "4px solid rgba(255,35,35,0.55)"
: effectiveStatus === 3
? "4px solid rgba(233,216,166,0.22)"
: "4px solid rgba(0,0,0,0)",
}} }}
> >
<div <div
onClick={() => cycleStatus(e)} onClick={() => cycleStatus(e)}
style={{ style={{
...styles.name, ...styles.name,
textDecoration: e.status === 1 ? "line-through" : "none", textDecoration: effectiveStatus === 1 ? "line-through" : "none",
color: getNameColor(e.status), color: getNameColor(effectiveStatus),
opacity: e.status === 1 ? 0.8 : 1, opacity: effectiveStatus === 1 ? 0.8 : 1,
}} }}
title="Klick: Grün → Rot → Grau → Leer" title="Klick: Grün → Rot → Grau → Leer"
> >
@@ -695,12 +927,12 @@ export default function App() {
<div style={styles.statusCell}> <div style={styles.statusCell}>
<span style={{ ...styles.statusBadge, color: badge.color, background: badge.background }}> <span style={{ ...styles.statusBadge, color: badge.color, background: badge.background }}>
{getStatusSymbol(e.status)} {getStatusSymbol(effectiveStatus)}
</span> </span>
</div> </div>
<button onClick={() => toggleTag(e)} style={styles.tagBtn} title="i → m → s → leer"> <button onClick={() => toggleTag(e)} style={styles.tagBtn} title="— → i → m → s.(Chip) → —">
{e.note_tag || "—"} {displayTag(e)}
</button> </button>
</div> </div>
); );
@@ -1152,7 +1384,6 @@ const styles = {
backgroundSize: "cover", backgroundSize: "cover",
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
/* kleines Dark-Wash, damit Schwarz/Gold lesbar ist */
filter: "saturate(0.9) contrast(1.05) brightness(0.55)", filter: "saturate(0.9) contrast(1.05) brightness(0.55)",
}, },
@@ -1164,14 +1395,59 @@ const styles = {
}, },
chipBtn: { chipBtn: {
padding: "10px 0", padding: "10px 14px",
borderRadius: 12, borderRadius: 12,
border: `1px solid rgba(233,216,166,0.22)`, border: "1px solid rgba(233,216,166,0.18)",
background: "rgba(255,255,255,0.06)", background: "rgba(255,255,255,0.06)",
color: stylesTokens.textGold, color: stylesTokens.textGold,
fontWeight: 1100, fontWeight: 1000,
cursor: "pointer",
minWidth: 64,
},
userBtn: {
display: "inline-flex",
alignItems: "center",
gap: 8,
padding: "10px 12px",
borderRadius: 12,
border: `1px solid rgba(233,216,166,0.18)`,
background: "rgba(255,255,255,0.05)",
color: stylesTokens.textMain,
fontWeight: 900,
cursor: "pointer", cursor: "pointer",
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)", boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)",
maxWidth: 180,
},
userDropdown: {
position: "absolute",
right: 0,
top: "calc(100% + 8px)",
minWidth: 220,
borderRadius: 14,
border: `1px solid rgba(233,216,166,0.18)`,
background: "linear-gradient(180deg, rgba(20,20,24,0.96), rgba(12,12,14,0.92))",
boxShadow: "0 18px 55px rgba(0,0,0,0.70)",
overflow: "hidden",
zIndex: 10000,
backdropFilter: "blur(8px)",
},
userDropdownItem: {
width: "100%",
textAlign: "left",
padding: "10px 12px",
border: "none",
background: "transparent",
color: stylesTokens.textMain,
fontWeight: 900,
cursor: "pointer",
},
userDropdownDivider: {
height: 1,
background: "rgba(233,216,166,0.12)",
}, },
}; };