Files
cluedo-hp-webapp/frontend/src/App.jsx
nessi 3d7d4c01f7 Update layout and scrolling styles for better responsiveness
Adjusted grid column sizing using "clamp" for improved adaptability to various screen sizes. Minor tweaks to sticky position and maximum height calculation ensure better alignment and consistent scroll behavior.
2026-02-07 11:17:54 +01:00

394 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef, useState } from "react";
import { api } from "./api/client";
import { cycleTag } from "./utils/cycleTag";
import { getChipLS, setChipLS, clearChipLS } from "./utils/chipStorage";
import { useHpGlobalStyles } from "./styles/hooks/useHpGlobalStyles";
import { styles } from "./styles/styles";
import { applyTheme, DEFAULT_THEME_KEY } from "./styles/themes";
import { stylesTokens } from "./styles/theme";
import LoginPage from "./components/LoginPage";
import SheetSection from "./components/SheetSection";
import ChipModal from "./components/ChipModal";
export default function App() {
useHpGlobalStyles();
// Auth/Login UI state
const [me, setMe] = useState(null);
const [loginEmail, setLoginEmail] = useState("");
const [loginPassword, setLoginPassword] = useState("");
const [showPw, setShowPw] = useState(false);
// Game/Sheet state (minimal)
const [games, setGames] = useState([]);
const [gameId, setGameId] = useState(null);
const [sheet, setSheet] = useState(null);
const [pulseId, setPulseId] = useState(null);
// Chip modal
const [chipOpen, setChipOpen] = useState(false);
const [chipEntry, setChipEntry] = useState(null);
// Live refresh (optional)
const aliveRef = useRef(true);
const load = async () => {
const m = await api("/auth/me");
setMe(m);
const tk = m?.theme_key || DEFAULT_THEME_KEY;
applyTheme(tk);
const gs = await api("/games");
setGames(gs);
// Auto-pick first game (kein UI dafür)
if (gs[0] && !gameId) setGameId(gs[0].id);
};
const reloadSheet = async () => {
if (!gameId) return;
const sh = await api(`/games/${gameId}/sheet`);
setSheet(sh);
};
// initial load
useEffect(() => {
aliveRef.current = true;
(async () => {
try {
await load();
} catch {
// ignore
}
})();
return () => {
aliveRef.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// on game change
useEffect(() => {
(async () => {
if (!gameId) return;
try {
await reloadSheet();
} catch {
// ignore
}
})();
}, [gameId]);
// Live refresh (nur Sheet)
useEffect(() => {
if (!me || !gameId) return;
let alive = true;
const tick = async () => {
try {
await reloadSheet();
} catch {}
};
tick();
const id = setInterval(() => {
if (!alive) return;
tick();
}, 2500);
return () => {
alive = false;
clearInterval(id);
};
}, [me?.id, gameId]);
// ===== Auth actions =====
const doLogin = async () => {
await api("/auth/login", {
method: "POST",
body: JSON.stringify({ email: loginEmail, password: loginPassword }),
});
await load();
};
// ===== Sheet actions (wie bisher) =====
const cycleStatus = async (entry) => {
let next = 0;
if (entry.status === 0) next = 2;
else if (entry.status === 2) next = 1;
else if (entry.status === 1) next = 3;
else next = 0;
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ status: next }),
});
await reloadSheet();
setPulseId(entry.entry_id);
setTimeout(() => setPulseId(null), 220);
};
const toggleTag = async (entry) => {
const next = cycleTag(entry.note_tag);
if (next === "s") {
setChipEntry(entry);
setChipOpen(true);
return;
}
if (next === null) clearChipLS(gameId, entry.entry_id);
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: next, chip: null }),
});
await reloadSheet();
};
const chooseChip = async (chip) => {
if (!chipEntry) return;
const entry = chipEntry;
setChipOpen(false);
setChipEntry(null);
setChipLS(gameId, entry.entry_id, chip);
try {
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: "s", chip }),
});
} finally {
await reloadSheet();
}
};
const closeChipModalToDash = async () => {
if (!chipEntry) {
setChipOpen(false);
return;
}
const entry = chipEntry;
setChipOpen(false);
setChipEntry(null);
clearChipLS(gameId, entry.entry_id);
try {
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
method: "PATCH",
body: JSON.stringify({ note_tag: null, chip: null }),
});
} finally {
await reloadSheet();
}
};
const displayTag = (entry) => {
const t = entry.note_tag;
if (!t) return "—";
if (t === "s") {
const chip = entry.chip || getChipLS(gameId, entry.entry_id);
return chip ? `s.${chip}` : "s";
}
return t; // i oder m
};
// ===== Login page =====
if (!me) {
return (
<LoginPage
loginEmail={loginEmail}
setLoginEmail={setLoginEmail}
loginPassword={loginPassword}
setLoginPassword={setLoginPassword}
showPw={showPw}
setShowPw={setShowPw}
doLogin={doLogin}
/>
);
}
const sections = sheet
? [
{ key: "suspect", title: "VERDÄCHTIGE PERSON", entries: sheet.suspect || [] },
{ key: "item", title: "GEGENSTAND", entries: sheet.item || [] },
{ key: "location", title: "ORT", entries: sheet.location || [] },
]
: [];
const PlaceholderCard = ({ title, hint }) => (
<div
style={{
borderRadius: 18,
border: `1px solid ${stylesTokens.panelBorder}`,
background: stylesTokens.panelBg,
boxShadow: "0 12px 30px rgba(0,0,0,0.35)",
backdropFilter: "blur(8px)",
padding: 12,
overflow: "hidden",
}}
>
<div style={{ fontWeight: 900, color: stylesTokens.textMain, fontSize: 13 }}>
{title}
</div>
<div style={{ marginTop: 6, color: stylesTokens.textDim, fontSize: 12, opacity: 0.95 }}>
{hint}
</div>
<div
style={{
marginTop: 10,
height: 64,
borderRadius: 14,
border: `1px dashed ${stylesTokens.panelBorder}`,
opacity: 0.8,
}}
/>
</div>
);
return (
<div style={styles.page}>
<div style={styles.bgFixed} aria-hidden="true">
<div style={styles.bgMap} />
</div>
{/* Layout: Links Game/Board, Rechts Notizen */}
<div
style={{
...styles.shell,
display: "grid",
gridTemplateColumns: "1fr clamp(420px, 28vw, 560px)",
gap: 14,
alignItems: "start",
}}
>
{/* LEFT: Game Area (Placeholders) */}
<div style={{ display: "grid", gap: 14 }}>
{/* Top bar placeholders (User + Settings) */}
<div style={{ display: "flex", gap: 10, justifyContent: "space-between" }}>
<PlaceholderCard title="User Dropdown" hint="(placeholder)" />
<PlaceholderCard title="Einstellungen" hint="(placeholder)" />
</div>
{/* Center Board */}
<div
style={{
borderRadius: 22,
border: `1px solid ${stylesTokens.panelBorder}`,
background: stylesTokens.panelBg,
boxShadow: "0 20px 70px rgba(0,0,0,0.45)",
backdropFilter: "blur(10px)",
padding: 12,
position: "relative",
overflow: "hidden",
minHeight: 420,
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(90deg, transparent, ${stylesTokens.goldLine}, transparent)`,
opacity: 0.25,
pointerEvents: "none",
}}
/>
<div style={{ position: "relative" }}>
<div style={{ fontWeight: 900, color: stylesTokens.textMain, fontSize: 14 }}>
3D Board / Game View
</div>
<div style={{ marginTop: 6, color: stylesTokens.textDim, fontSize: 12 }}>
Platzhalter hier kommt später das Board + Figuren rein.
</div>
<div
style={{
marginTop: 10,
height: 360,
borderRadius: 18,
border: `1px dashed ${stylesTokens.panelBorder}`,
opacity: 0.85,
}}
/>
</div>
</div>
{/* Bottom row placeholders */}
<div
style={{
display: "grid",
gridTemplateColumns: "1.1fr 0.8fr 1.1fr",
gap: 14,
}}
>
<PlaceholderCard title="Meine Geheimkarten" hint="(placeholder)" />
<PlaceholderCard title="Würfel + Hogwarts Points" hint="(placeholder)" />
<PlaceholderCard title="Spielerkarte / Turn" hint="(placeholder)" />
</div>
{/* Extra: Hilfskarten / Deck */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
<PlaceholderCard title="Dunkles Deck" hint="(placeholder)" />
<PlaceholderCard title="Hilfskarten" hint="(placeholder)" />
</div>
</div>
{/* RIGHT: Notes Panel */}
<div
style={{
position: "sticky",
top: 14,
alignSelf: "start",
borderRadius: 22,
border: `1px solid ${stylesTokens.panelBorder}`,
background: stylesTokens.panelBg,
boxShadow: "0 22px 90px rgba(0,0,0,0.55)",
backdropFilter: "blur(10px)",
padding: 12,
maxHeight: "calc(100vh - 28px)",
overflow: "auto",
}}
>
<div style={{ fontWeight: 900, color: stylesTokens.textMain, fontSize: 14 }}>
Notizen
</div>
<div style={{ marginTop: 6, color: stylesTokens.textDim, fontSize: 12 }}>
Nur die 3 Tabellen (Verdächtige / Gegenstände / Orte).
</div>
<div style={{ marginTop: 12, 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>
</div>
<ChipModal
chipOpen={chipOpen}
closeChipModalToDash={closeChipModalToDash}
chooseChip={chooseChip}
/>
</div>
);
}