Merge dev branch into main brunch #1
@@ -1,36 +1,42 @@
|
|||||||
|
// src/App.jsx
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { api } from "./api/client";
|
import { api } from "./api/client";
|
||||||
import { cycleTag } from "./utils/cycleTag";
|
import { cycleTag } from "./utils/cycleTag";
|
||||||
import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage";
|
import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage";
|
||||||
import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles";
|
import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles";
|
||||||
import { styles } from "./styles/styles";
|
import { styles } from "./styles/styles";
|
||||||
import { stylesTokens } from "./styles/theme";
|
|
||||||
|
|
||||||
import AdminPanel from "./components/AdminPanel";
|
import AdminPanel from "./components/AdminPanel";
|
||||||
import LoginPage from "./components/LoginPage";
|
import LoginPage from "./components/LoginPage";
|
||||||
import TopBar from "./components/TopBar";
|
import TopBar from "./components/TopBar";
|
||||||
import PasswordModal from "./components/PasswordModal";
|
import PasswordModal from "./components/PasswordModal";
|
||||||
import ChipModal from "./components/ChipModal";
|
import ChipModal from "./components/ChipModal";
|
||||||
// HelpModal + SheetSection würdest du analog auslagern
|
import HelpModal from "./components/HelpModal";
|
||||||
|
import GamePickerCard from "./components/GamePickerCard";
|
||||||
|
import SheetSection from "./components/SheetSection";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
useHpGlobalStyles();
|
useHpGlobalStyles();
|
||||||
|
|
||||||
|
// Auth/Login UI state
|
||||||
const [me, setMe] = useState(null);
|
const [me, setMe] = useState(null);
|
||||||
const [loginEmail, setLoginEmail] = useState("");
|
const [loginEmail, setLoginEmail] = useState("");
|
||||||
const [loginPassword, setLoginPassword] = useState("");
|
const [loginPassword, setLoginPassword] = useState("");
|
||||||
const [showPw, setShowPw] = useState(false);
|
const [showPw, setShowPw] = useState(false);
|
||||||
|
|
||||||
|
// Game/Sheet state
|
||||||
const [games, setGames] = useState([]);
|
const [games, setGames] = useState([]);
|
||||||
const [gameId, setGameId] = useState(null);
|
const [gameId, setGameId] = useState(null);
|
||||||
const [sheet, setSheet] = useState(null);
|
const [sheet, setSheet] = useState(null);
|
||||||
const [pulseId, setPulseId] = useState(null);
|
const [pulseId, setPulseId] = useState(null);
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
|
|
||||||
const [chipOpen, setChipOpen] = useState(false);
|
const [chipOpen, setChipOpen] = useState(false);
|
||||||
const [chipEntry, setChipEntry] = useState(null);
|
const [chipEntry, setChipEntry] = useState(null);
|
||||||
|
|
||||||
const [helpOpen, setHelpOpen] = useState(false);
|
|
||||||
|
|
||||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||||
|
|
||||||
const [pwOpen, setPwOpen] = useState(false);
|
const [pwOpen, setPwOpen] = useState(false);
|
||||||
@@ -39,6 +45,7 @@ export default function App() {
|
|||||||
const [pwMsg, setPwMsg] = useState("");
|
const [pwMsg, setPwMsg] = useState("");
|
||||||
const [pwSaving, setPwSaving] = useState(false);
|
const [pwSaving, setPwSaving] = useState(false);
|
||||||
|
|
||||||
|
// ===== Data loaders =====
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const m = await api("/auth/me");
|
const m = await api("/auth/me");
|
||||||
setMe(m);
|
setMe(m);
|
||||||
@@ -49,6 +56,14 @@ export default function App() {
|
|||||||
if (gs[0] && !gameId) setGameId(gs[0].id);
|
if (gs[0] && !gameId) setGameId(gs[0].id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reloadSheet = async () => {
|
||||||
|
if (!gameId) return;
|
||||||
|
const sh = await api(`/games/${gameId}/sheet`);
|
||||||
|
setSheet(sh);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== Effects =====
|
||||||
|
|
||||||
// Dropdown outside click
|
// Dropdown outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onDown = (e) => {
|
const onDown = (e) => {
|
||||||
@@ -59,25 +74,32 @@ export default function App() {
|
|||||||
return () => document.removeEventListener("mousedown", onDown);
|
return () => document.removeEventListener("mousedown", onDown);
|
||||||
}, [userMenuOpen]);
|
}, [userMenuOpen]);
|
||||||
|
|
||||||
|
// initial load (try session)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await load();
|
await load();
|
||||||
} catch {}
|
} catch {
|
||||||
|
// not logged in
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// load sheet when game changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!gameId) return;
|
if (!gameId) return;
|
||||||
try {
|
try {
|
||||||
const sh = await api(`/games/${gameId}/sheet`);
|
await reloadSheet();
|
||||||
setSheet(sh);
|
} catch {
|
||||||
} catch {}
|
// ignore
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [gameId]);
|
}, [gameId]);
|
||||||
|
|
||||||
|
// ===== Auth actions =====
|
||||||
const doLogin = async () => {
|
const doLogin = async () => {
|
||||||
await api("/auth/login", {
|
await api("/auth/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -94,7 +116,7 @@ export default function App() {
|
|||||||
setSheet(null);
|
setSheet(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Password change
|
// ===== Password change =====
|
||||||
const openPwModal = () => {
|
const openPwModal = () => {
|
||||||
setPwMsg("");
|
setPwMsg("");
|
||||||
setPw1("");
|
setPw1("");
|
||||||
@@ -112,6 +134,7 @@ export default function App() {
|
|||||||
|
|
||||||
const savePassword = async () => {
|
const savePassword = async () => {
|
||||||
setPwMsg("");
|
setPwMsg("");
|
||||||
|
|
||||||
if (!pw1 || pw1.length < 8) return setPwMsg("❌ Passwort muss mindestens 8 Zeichen haben.");
|
if (!pw1 || pw1.length < 8) return setPwMsg("❌ Passwort muss mindestens 8 Zeichen haben.");
|
||||||
if (pw1 !== pw2) return setPwMsg("❌ Passwörter stimmen nicht überein.");
|
if (pw1 !== pw2) return setPwMsg("❌ Passwörter stimmen nicht überein.");
|
||||||
|
|
||||||
@@ -130,6 +153,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== Game actions =====
|
||||||
const newGame = async () => {
|
const newGame = async () => {
|
||||||
const g = await api("/games", {
|
const g = await api("/games", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -140,12 +164,7 @@ export default function App() {
|
|||||||
setGameId(g.id);
|
setGameId(g.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadSheet = async () => {
|
// ===== Sheet actions =====
|
||||||
if (!gameId) return;
|
|
||||||
const sh = await api(`/games/${gameId}/sheet`);
|
|
||||||
setSheet(sh);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cycleStatus = async (entry) => {
|
const cycleStatus = async (entry) => {
|
||||||
let next = 0;
|
let next = 0;
|
||||||
if (entry.status === 0) next = 2;
|
if (entry.status === 0) next = 2;
|
||||||
@@ -166,18 +185,21 @@ export default function App() {
|
|||||||
const toggleTag = async (entry) => {
|
const toggleTag = async (entry) => {
|
||||||
const next = cycleTag(entry.note_tag);
|
const next = cycleTag(entry.note_tag);
|
||||||
|
|
||||||
|
// going to "s" -> open chip modal, don't write backend yet
|
||||||
if (next === "s") {
|
if (next === "s") {
|
||||||
setChipEntry(entry);
|
setChipEntry(entry);
|
||||||
setChipOpen(true);
|
setChipOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// s -> — : clear local chip
|
||||||
if (next === null) clearChipLS(gameId, entry.entry_id);
|
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,9 +210,11 @@ export default function App() {
|
|||||||
setChipOpen(false);
|
setChipOpen(false);
|
||||||
setChipEntry(null);
|
setChipEntry(null);
|
||||||
|
|
||||||
|
// frontend-only save
|
||||||
setChipLS(gameId, entry.entry_id, chip);
|
setChipLS(gameId, entry.entry_id, chip);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// backend only gets "s"
|
||||||
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: "s" }),
|
body: JSON.stringify({ note_tag: "s" }),
|
||||||
@@ -229,7 +253,7 @@ export default function App() {
|
|||||||
const chip = getChipLS(gameId, entry.entry_id);
|
const chip = getChipLS(gameId, entry.entry_id);
|
||||||
return chip ? `s.${chip}` : "s";
|
return chip ? `s.${chip}` : "s";
|
||||||
}
|
}
|
||||||
return t;
|
return t; // i oder m
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Login page =====
|
// ===== Login page =====
|
||||||
@@ -273,8 +297,30 @@ export default function App() {
|
|||||||
|
|
||||||
{me.role === "admin" && <AdminPanel />}
|
{me.role === "admin" && <AdminPanel />}
|
||||||
|
|
||||||
{/* hier bleiben vorerst: Spiel selector + Help + Sections
|
<GamePickerCard
|
||||||
-> kannst du als nächsten Schritt in Komponenten auslagern */}
|
games={games}
|
||||||
|
gameId={gameId}
|
||||||
|
setGameId={setGameId}
|
||||||
|
onOpenHelp={() => setHelpOpen(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||||
|
|
||||||
|
<div style={{ marginTop: 14, display: "grid", gap: 14 }}>
|
||||||
|
{sections.map((sec) => (
|
||||||
|
<SheetSection
|
||||||
|
key={sec.key}
|
||||||
|
title={sec.title}
|
||||||
|
entries={sec.entries}
|
||||||
|
pulseId={pulseId}
|
||||||
|
onCycleStatus={cycleStatus}
|
||||||
|
onToggleTag={toggleTag}
|
||||||
|
displayTag={displayTag}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ height: 24 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasswordModal
|
<PasswordModal
|
||||||
@@ -289,7 +335,11 @@ export default function App() {
|
|||||||
savePassword={savePassword}
|
savePassword={savePassword}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChipModal chipOpen={chipOpen} closeChipModalToDash={closeChipModalToDash} chooseChip={chooseChip} />
|
<ChipModal
|
||||||
|
chipOpen={chipOpen}
|
||||||
|
closeChipModalToDash={closeChipModalToDash}
|
||||||
|
chooseChip={chooseChip}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
frontend/src/components/GamePickerCard.jsx
Normal file
36
frontend/src/components/GamePickerCard.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// src/components/GamePickerCard.jsx
|
||||||
|
import React from "react";
|
||||||
|
import { styles } from "../styles/styles";
|
||||||
|
|
||||||
|
export default function GamePickerCard({
|
||||||
|
games,
|
||||||
|
gameId,
|
||||||
|
setGameId,
|
||||||
|
onOpenHelp,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 14 }}>
|
||||||
|
<div style={styles.card}>
|
||||||
|
<div style={styles.sectionHeader}>Spiel</div>
|
||||||
|
|
||||||
|
<div style={styles.cardBody}>
|
||||||
|
<select
|
||||||
|
value={gameId || ""}
|
||||||
|
onChange={(e) => setGameId(e.target.value)}
|
||||||
|
style={{ ...styles.input, flex: 1 }}
|
||||||
|
>
|
||||||
|
{games.map((g) => (
|
||||||
|
<option key={g.id} value={g.id}>
|
||||||
|
{g.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button onClick={onOpenHelp} style={styles.helpBtn} title="Hilfe">
|
||||||
|
Hilfe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
134
frontend/src/components/HelpModal.jsx
Normal file
134
frontend/src/components/HelpModal.jsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// src/components/HelpModal.jsx
|
||||||
|
import React from "react";
|
||||||
|
import { styles } from "../styles/styles";
|
||||||
|
import { stylesTokens } from "../styles/theme";
|
||||||
|
|
||||||
|
export default function HelpModal({ open, onClose }) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.modalOverlay} onMouseDown={onClose}>
|
||||||
|
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
||||||
|
<div style={styles.modalHeader}>
|
||||||
|
<div style={{ fontWeight: 1000, color: stylesTokens.textGold }}>Hilfe</div>
|
||||||
|
<button onClick={onClose} style={styles.modalCloseBtn} aria-label="Schließen">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpBody}>
|
||||||
|
<div style={styles.helpSectionTitle}>1) Namen anklicken (Status)</div>
|
||||||
|
<div style={styles.helpText}>
|
||||||
|
Tippe auf einen Namen, um den Status zu wechseln. Reihenfolge:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpList}>
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...styles.helpBadge,
|
||||||
|
background: "rgba(0,190,80,0.18)",
|
||||||
|
color: "#baf3c9",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<b>Grün</b> = bestätigt / fix richtig
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...styles.helpBadge,
|
||||||
|
background: "rgba(255,35,35,0.18)",
|
||||||
|
color: "#ffb3b3",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<b>Rot</b> = ausgeschlossen / fix falsch
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...styles.helpBadge,
|
||||||
|
background: "rgba(140,140,140,0.14)",
|
||||||
|
color: "rgba(233,216,166,0.85)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<b>Grau</b> = unsicher / „vielleicht“
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...styles.helpBadge,
|
||||||
|
background: "rgba(255,255,255,0.08)",
|
||||||
|
color: "rgba(233,216,166,0.75)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
–
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<b>Leer</b> = unknown / noch nicht bewertet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpDivider} />
|
||||||
|
|
||||||
|
<div style={styles.helpSectionTitle}>2) i / m / s Button (Notiz)</div>
|
||||||
|
<div style={styles.helpText}>
|
||||||
|
Rechts pro Zeile gibt es einen Button, der durch diese Werte rotiert:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpList}>
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span style={styles.helpMiniTag}>i</span>
|
||||||
|
<div>
|
||||||
|
<b>i</b> = „Ich habe diese Geheimkarte“
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span style={styles.helpMiniTag}>m</span>
|
||||||
|
<div>
|
||||||
|
<b>m</b> = „Geheimkarte aus dem mittleren Deck“
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span style={styles.helpMiniTag}>s</span>
|
||||||
|
<div>
|
||||||
|
<b>s</b> = „Ein anderer Spieler hat diese Karte“ (Chip Auswahl)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpListRow}>
|
||||||
|
<span style={styles.helpMiniTag}>—</span>
|
||||||
|
<div>
|
||||||
|
<b>—</b> = keine Notiz
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.helpDivider} />
|
||||||
|
|
||||||
|
<div style={styles.helpText}>
|
||||||
|
Tipp: Jeder Spieler sieht nur seine eigenen Notizen – andere Spieler können nicht in
|
||||||
|
deinen Zettel schauen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
frontend/src/components/SheetSection.jsx
Normal file
121
frontend/src/components/SheetSection.jsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// src/components/SheetSection.jsx
|
||||||
|
import React from "react";
|
||||||
|
import { styles } from "../styles/styles";
|
||||||
|
import { stylesTokens } from "../styles/theme";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* props:
|
||||||
|
* - title: string
|
||||||
|
* - entries: array
|
||||||
|
* - pulseId: number | null
|
||||||
|
* - onCycleStatus(entry): fn
|
||||||
|
* - onToggleTag(entry): fn
|
||||||
|
* - displayTag(entry): string
|
||||||
|
*/
|
||||||
|
export default function SheetSection({
|
||||||
|
title,
|
||||||
|
entries,
|
||||||
|
pulseId,
|
||||||
|
onCycleStatus,
|
||||||
|
onToggleTag,
|
||||||
|
displayTag,
|
||||||
|
}) {
|
||||||
|
// --- helpers (lokal, weil sie rein UI sind) ---
|
||||||
|
const getRowBg = (status) => {
|
||||||
|
if (status === 1) return "rgba(255, 35, 35, 0.16)";
|
||||||
|
if (status === 2) return "rgba(0, 190, 80, 0.16)";
|
||||||
|
if (status === 3) return "rgba(140, 140, 140, 0.12)";
|
||||||
|
return "rgba(255,255,255,0.06)";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNameColor = (status) => {
|
||||||
|
if (status === 1) return "#ffb3b3";
|
||||||
|
if (status === 2) return "#baf3c9";
|
||||||
|
if (status === 3) return "rgba(233,216,166,0.78)";
|
||||||
|
return stylesTokens.textMain;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusSymbol = (status) => {
|
||||||
|
if (status === 2) return "✓";
|
||||||
|
if (status === 1) return "✕";
|
||||||
|
if (status === 3) return "?";
|
||||||
|
return "–";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status) => {
|
||||||
|
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 === 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 (
|
||||||
|
<div style={styles.card}>
|
||||||
|
<div style={styles.sectionHeader}>{title}</div>
|
||||||
|
|
||||||
|
<div style={{ display: "grid" }}>
|
||||||
|
{entries.map((e) => {
|
||||||
|
// UI "rot" wenn note_tag i/m/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 (
|
||||||
|
<div
|
||||||
|
key={e.entry_id}
|
||||||
|
className="hp-row"
|
||||||
|
style={{
|
||||||
|
...styles.row,
|
||||||
|
background: getRowBg(effectiveStatus),
|
||||||
|
animation: pulseId === e.entry_id ? "rowPulse 220ms ease-out" : "none",
|
||||||
|
borderLeft:
|
||||||
|
effectiveStatus === 2
|
||||||
|
? "4px solid rgba(0,190,80,0.55)"
|
||||||
|
: effectiveStatus === 1
|
||||||
|
? "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
|
||||||
|
onClick={() => onCycleStatus(e)}
|
||||||
|
style={{
|
||||||
|
...styles.name,
|
||||||
|
textDecoration: effectiveStatus === 1 ? "line-through" : "none",
|
||||||
|
color: getNameColor(effectiveStatus),
|
||||||
|
opacity: effectiveStatus === 1 ? 0.8 : 1,
|
||||||
|
}}
|
||||||
|
title="Klick: Grün → Rot → Grau → Leer"
|
||||||
|
>
|
||||||
|
{e.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.statusCell}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...styles.statusBadge,
|
||||||
|
color: badge.color,
|
||||||
|
background: badge.background,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getStatusSymbol(effectiveStatus)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleTag(e)}
|
||||||
|
style={styles.tagBtn}
|
||||||
|
title="— → i → m → s.(Chip) → —"
|
||||||
|
>
|
||||||
|
{displayTag(e)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user