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.
394 lines
11 KiB
JavaScript
394 lines
11 KiB
JavaScript
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>
|
||
);
|
||
}
|