From 1932e837fb4d7c1d5e111d0b7ea41cc6340b6ee6 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 14 Mar 2026 11:46:28 +0100 Subject: [PATCH] Extract PlayerCharacterSection component from App.tsx Move player character modal state (createPlayerOpen, managementOpen, editingPlayer) into a self-contained component with an imperative ref handle. Closing the create/edit modal now returns to management. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/App.tsx | 62 ++++--------- .../components/player-character-section.tsx | 91 +++++++++++++++++++ 2 files changed, 107 insertions(+), 46 deletions(-) create mode 100644 apps/web/src/components/player-character-section.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 3952cbf..87223b1 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -17,8 +17,10 @@ import { } from "react"; import { ActionBar } from "./components/action-bar"; import { CombatantRow } from "./components/combatant-row"; -import { CreatePlayerModal } from "./components/create-player-modal"; -import { PlayerManagement } from "./components/player-management"; +import { + PlayerCharacterSection, + type PlayerCharacterSectionHandle, +} from "./components/player-character-section"; import { StatBlockPanel } from "./components/stat-block-panel"; import { Toast } from "./components/toast"; import { TurnNavigation } from "./components/turn-navigation"; @@ -97,12 +99,6 @@ export function App() { deleteCharacter: deletePlayerCharacter, } = usePlayerCharacters(); - const [createPlayerOpen, setCreatePlayerOpen] = useState(false); - const [managementOpen, setManagementOpen] = useState(false); - const [editingPlayer, setEditingPlayer] = useState< - (typeof playerCharacters)[number] | undefined - >(undefined); - const { search, getCreature, @@ -184,6 +180,7 @@ export function App() { }, [sidePanel.dismissPanel, bulkImport.reset]); const actionBarInputRef = useRef(null); + const playerCharacterRef = useRef(null); const actionBarAnim = useActionBarAnimation(encounter.combatants.length); // Auto-scroll to the active combatant when the turn changes @@ -256,7 +253,9 @@ export function App() { inputRef={actionBarInputRef} playerCharacters={playerCharacters} onAddFromPlayerCharacter={addFromPlayerCharacter} - onManagePlayers={() => setManagementOpen(true)} + onManagePlayers={() => + playerCharacterRef.current?.openManagement() + } onRollAllInitiative={handleRollAllInitiative} showRollAllInitiative={hasCreatureCombatants} rollAllInitiativeDisabled={!canRollAllInitiative} @@ -313,7 +312,9 @@ export function App() { inputRef={actionBarInputRef} playerCharacters={playerCharacters} onAddFromPlayerCharacter={addFromPlayerCharacter} - onManagePlayers={() => setManagementOpen(true)} + onManagePlayers={() => + playerCharacterRef.current?.openManagement() + } onRollAllInitiative={handleRollAllInitiative} showRollAllInitiative={hasCreatureCombatants} rollAllInitiativeDisabled={!canRollAllInitiative} @@ -403,43 +404,12 @@ export function App() { /> )} - { - setCreatePlayerOpen(false); - setEditingPlayer(undefined); - }} - onSave={(name, ac, maxHp, color, icon) => { - if (editingPlayer) { - editPlayerCharacter?.(editingPlayer.id, { - name, - ac, - maxHp, - color: color ?? null, - icon: icon ?? null, - }); - } else { - createPlayerCharacter(name, ac, maxHp, color, icon); - } - }} - playerCharacter={editingPlayer} - /> - - setManagementOpen(false)} + { - setEditingPlayer(pc); - setCreatePlayerOpen(true); - setManagementOpen(false); - }} - onDelete={(id) => deletePlayerCharacter?.(id)} - onCreate={() => { - setEditingPlayer(undefined); - setCreatePlayerOpen(true); - setManagementOpen(false); - }} + onCreateCharacter={createPlayerCharacter} + onEditCharacter={editPlayerCharacter} + onDeleteCharacter={deletePlayerCharacter} /> ); diff --git a/apps/web/src/components/player-character-section.tsx b/apps/web/src/components/player-character-section.tsx new file mode 100644 index 0000000..e7b7fd1 --- /dev/null +++ b/apps/web/src/components/player-character-section.tsx @@ -0,0 +1,91 @@ +import type { PlayerCharacter, PlayerCharacterId } from "@initiative/domain"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { CreatePlayerModal } from "./create-player-modal.js"; +import { PlayerManagement } from "./player-management.js"; + +export interface PlayerCharacterSectionHandle { + openManagement: () => void; +} + +interface PlayerCharacterSectionProps { + characters: readonly PlayerCharacter[]; + onCreateCharacter: ( + name: string, + ac: number, + maxHp: number, + color: string | undefined, + icon: string | undefined, + ) => void; + onEditCharacter: ( + id: PlayerCharacterId, + fields: { + name?: string; + ac?: number; + maxHp?: number; + color?: string | null; + icon?: string | null; + }, + ) => void; + onDeleteCharacter: (id: PlayerCharacterId) => void; +} + +export const PlayerCharacterSection = forwardRef< + PlayerCharacterSectionHandle, + PlayerCharacterSectionProps +>(function PlayerCharacterSection( + { characters, onCreateCharacter, onEditCharacter, onDeleteCharacter }, + ref, +) { + const [managementOpen, setManagementOpen] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const [editingPlayer, setEditingPlayer] = useState< + PlayerCharacter | undefined + >(); + + useImperativeHandle(ref, () => ({ + openManagement: () => setManagementOpen(true), + })); + + return ( + <> + { + setCreateOpen(false); + setEditingPlayer(undefined); + setManagementOpen(true); + }} + onSave={(name, ac, maxHp, color, icon) => { + if (editingPlayer) { + onEditCharacter(editingPlayer.id, { + name, + ac, + maxHp, + color: color ?? null, + icon: icon ?? null, + }); + } else { + onCreateCharacter(name, ac, maxHp, color, icon); + } + }} + playerCharacter={editingPlayer} + /> + setManagementOpen(false)} + characters={characters} + onEdit={(pc) => { + setEditingPlayer(pc); + setCreateOpen(true); + setManagementOpen(false); + }} + onDelete={(id) => onDeleteCharacter(id)} + onCreate={() => { + setEditingPlayer(undefined); + setCreateOpen(true); + setManagementOpen(false); + }} + /> + + ); +});