From 247379ba57a52dd0ea3c21eb5c544581f3a8f5a6 Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sat, 11 Apr 2026 15:48:41 -0400 Subject: [PATCH] feat: integrate initiative tracker into campaign view Wires up InitiativeTracker and CombatStartModal into CampaignView with socket event handlers, a DM-only Combat button, two-column layout with combat sidebar, and responsive CSS. Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/CampaignView.module.css | 51 +++++++++++++++ client/src/pages/CampaignView.tsx | 79 +++++++++++++++++++----- 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/client/src/pages/CampaignView.module.css b/client/src/pages/CampaignView.module.css index fa2a770..86f0682 100644 --- a/client/src/pages/CampaignView.module.css +++ b/client/src/pages/CampaignView.module.css @@ -165,3 +165,54 @@ font-style: italic; grid-column: 1 / -1; } + +/* ── Combat layout ── */ + +.content { + display: block; +} + +.content.withCombat { + display: flex; + gap: 0; + align-items: flex-start; +} + +.combatSidebar { + width: 190px; + flex-shrink: 0; + border-right: 1px solid rgba(var(--gold-rgb), 0.15); + padding-right: 0.5rem; + margin-right: 0.75rem; + min-height: 300px; + position: sticky; + top: 0; +} + +.content.withCombat .grid { + flex: 1; + min-width: 0; +} + +.addBtnActive { + background: rgba(var(--gold-rgb), 0.15) !important; + border: 1px solid rgba(var(--gold-rgb), 0.5) !important; + color: var(--gold) !important; +} + +@media (max-width: 768px) { + .content.withCombat { + flex-direction: column; + } + + .combatSidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid rgba(var(--gold-rgb), 0.15); + padding-right: 0; + margin-right: 0; + padding-bottom: 0.5rem; + margin-bottom: 0.75rem; + position: static; + } +} diff --git a/client/src/pages/CampaignView.tsx b/client/src/pages/CampaignView.tsx index ecf1669..167b999 100644 --- a/client/src/pages/CampaignView.tsx +++ b/client/src/pages/CampaignView.tsx @@ -18,7 +18,9 @@ import { undoRoll, } from "../api"; import { useAuth } from "../context/AuthContext"; -import type { Character, Gear, Talent, GameItem, RollResult } from "../types"; +import type { Character, Gear, Talent, GameItem, RollResult, CombatState } from "../types.js"; +import InitiativeTracker from "../components/InitiativeTracker.js"; +import CombatStartModal from "../components/CombatStartModal.js"; import CharacterCard from "../components/CharacterCard"; import CharacterDetail from "../components/CharacterDetail"; import RollLog from "../components/RollLog"; @@ -54,6 +56,8 @@ export default function CampaignView() { const pendingRollRef = useRef(null); const [atmosphere, setAtmosphere] = useState(defaultAtmosphere); const [focusSpells, setFocusSpells] = useState>(new Map()); + const [combats, setCombats] = useState([]); + const [showCombatStart, setShowCombatStart] = useState(false); function handleAtmosphereChange(next: AtmosphereState) { setAtmosphere(next); @@ -70,6 +74,7 @@ export default function CampaignView() { if (c) setCampaignName(c.name); }); socket.emit("join-campaign", String(campaignId)); + socket.emit("initiative:request-state", campaignId); return () => { socket.emit("leave-campaign", String(campaignId)); }; @@ -256,6 +261,14 @@ export default function CampaignView() { void characterId; } + function onInitiativeState(data: CombatState[]) { + setCombats(data); + } + + function onInitiativeUpdated(data: CombatState[]) { + setCombats(data); + } + socket.on("character:created", onCharacterCreated); socket.on("character:updated", onCharacterUpdated); socket.on("character:deleted", onCharacterDeleted); @@ -268,6 +281,8 @@ export default function CampaignView() { socket.on("atmosphere:update", onAtmosphereUpdate); socket.on("roll:undone", onRollUndone); socket.on("character:rested", onCharacterRested); + socket.on("initiative:state", onInitiativeState); + socket.on("initiative:updated", onInitiativeUpdated); return () => { socket.off("character:created", onCharacterCreated); @@ -282,6 +297,8 @@ export default function CampaignView() { socket.off("atmosphere:update", onAtmosphereUpdate); socket.off("roll:undone", onRollUndone); socket.off("character:rested", onCharacterRested); + socket.off("initiative:state", onInitiativeState); + socket.off("initiative:updated", onInitiativeUpdated); }; }, []); @@ -387,6 +404,14 @@ export default function CampaignView() { onAtmosphereChange={handleAtmosphereChange} /> )} + {role === "dm" && ( + + )} {role === "dm" && (