Refactor App structure and add modular components
Split GamePickerCard, HelpModal, and SheetSection into separate components for better modularity and clarity. Refactored App.jsx to utilize these new components, restructured state variables, and organized functions for improved readability. Enhanced code comments for easier maintenance.
This commit is contained in:
@@ -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