import { useState, useRef, useEffect } from "react"; import type { Character, GameItem } from "../types"; import { calculateAC } from "../utils/derived-ac"; import AcDisplay from "./AcDisplay"; import InlineNumber from "./InlineNumber"; import StatsPanel from "./StatsPanel"; import InfoPanel from "./InfoPanel"; import GearPanel from "./GearPanel"; import styles from "./CharacterSheet.module.css"; interface CharacterSheetProps { character: Character; mode: "view" | "edit"; campaignId: number; onUpdate: (id: number, data: Partial) => void; onStatChange: (characterId: number, statName: string, value: number) => void; onAddGearFromItem: (characterId: number, item: GameItem) => void; onAddGearCustom: ( characterId: number, data: { name: string; type: string; slot_count: number }, ) => void; onRemoveGear: (characterId: number, gearId: number) => void; onAddTalent: ( characterId: number, data: { name: string; description: string; effect?: Record; game_talent_id?: number | null; }, ) => void; onRemoveTalent: (characterId: number, talentId: number) => void; onDelete: (id: number) => void; } export default function CharacterSheet({ character, mode, campaignId, onUpdate, onStatChange, onAddGearFromItem, onAddGearCustom, onRemoveGear, onAddTalent, onRemoveTalent, onDelete, }: CharacterSheetProps) { const [confirmDelete, setConfirmDelete] = useState(false); const debounceRef = useRef>(); const acBreakdown = calculateAC(character); const xpThreshold = character.level * 10; useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, []); function handleNameField(field: string, value: string) { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { onUpdate(character.id, { [field]: value }); }, 400); } function handleAcOverride(value: number | null) { const overrides = { ...(character.overrides || {}) }; if (value === null) { delete overrides.ac; } else { overrides.ac = value; } onUpdate(character.id, { overrides } as Partial); } return ( <> {/* HEADER BANNER */}
{mode === "edit" ? (
handleNameField("name", e.target.value)} /> handleNameField("title", e.target.value)} />
) : (
{character.name} {character.title && ( {character.title} )}
)}
Level {character.level} {character.ancestry} {character.class}
{/* HP — click to edit */}
HP onUpdate(character.id, { hp_current: hp })} className={`${styles.vitalValue} ${styles.hp}`} /> / {mode === "edit" ? ( onUpdate(character.id, { hp_max: hp })} className={styles.hpMax} min={0} /> ) : ( {character.hp_max} )}
{/* AC — display only in view, override in edit */}
{/* XP — click to edit */}
XP onUpdate(character.id, { xp })} className={`${styles.vitalValue} ${styles.xpCurrent}`} min={0} /> / {xpThreshold}
{/* THREE PANELS */}
onUpdate(charId, { [field]: value }) } />
{/* DELETE — edit mode only */} {mode === "edit" && (
{confirmDelete ? (
Delete {character.name}? {" "}
) : ( )}
)} ); }