import type { CombatantId } from "@initiative/domain"; import { type FormEvent, useCallback, useRef, useState } from "react"; import { useEncounter } from "./hooks/use-encounter"; function formatEvent(e: ReturnType["events"][number]) { switch (e.type) { case "TurnAdvanced": return `Turn: ${e.previousCombatantId} → ${e.newCombatantId} (round ${e.roundNumber})`; case "RoundAdvanced": return `Round advanced to ${e.newRoundNumber}`; case "CombatantAdded": return `Added combatant: ${e.name}`; case "CombatantRemoved": return `Removed combatant: ${e.name}`; case "CombatantUpdated": return `Renamed combatant: ${e.oldName} → ${e.newName}`; case "InitiativeSet": return `Initiative: ${e.combatantId} ${e.previousValue ?? "unset"} → ${e.newValue ?? "unset"}`; case "MaxHpSet": return `Max HP: ${e.combatantId} ${e.previousMaxHp ?? "unset"} → ${e.newMaxHp ?? "unset"}`; case "CurrentHpAdjusted": return `HP: ${e.combatantId} ${e.previousHp} → ${e.newHp} (${e.delta > 0 ? "+" : ""}${e.delta})`; } } function EditableName({ name, combatantId, isActive, onRename, }: { name: string; combatantId: CombatantId; isActive: boolean; onRename: (id: CombatantId, newName: string) => void; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(name); const inputRef = useRef(null); const commit = useCallback(() => { const trimmed = draft.trim(); if (trimmed !== "" && trimmed !== name) { onRename(combatantId, trimmed); } setEditing(false); }, [draft, name, combatantId, onRename]); const startEditing = useCallback(() => { setDraft(name); setEditing(true); requestAnimationFrame(() => inputRef.current?.select()); }, [name]); if (editing) { return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} /> ); } return ( ); } function MaxHpInput({ maxHp, onCommit, }: { maxHp: number | undefined; onCommit: (value: number | undefined) => void; }) { const [draft, setDraft] = useState(maxHp?.toString() ?? ""); const prev = useRef(maxHp); // Sync draft when domain value changes externally (e.g. from another source) if (maxHp !== prev.current) { prev.current = maxHp; setDraft(maxHp?.toString() ?? ""); } const commit = useCallback(() => { if (draft === "") { onCommit(undefined); return; } const n = Number.parseInt(draft, 10); if (!Number.isNaN(n) && n >= 1) { onCommit(n); } else { // Revert invalid input setDraft(maxHp?.toString() ?? ""); } }, [draft, maxHp, onCommit]); return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") commit(); }} /> ); } function CurrentHpInput({ currentHp, maxHp, onCommit, }: { currentHp: number | undefined; maxHp: number | undefined; onCommit: (value: number) => void; }) { const [draft, setDraft] = useState(currentHp?.toString() ?? ""); const prev = useRef(currentHp); if (currentHp !== prev.current) { prev.current = currentHp; setDraft(currentHp?.toString() ?? ""); } const commit = useCallback(() => { if (draft === "" || currentHp === undefined) return; const n = Number.parseInt(draft, 10); if (!Number.isNaN(n)) { onCommit(n); } else { setDraft(currentHp.toString()); } }, [draft, currentHp, onCommit]); return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={(e) => { if (e.key === "Enter") commit(); }} /> ); } export function App() { const { encounter, events, advanceTurn, addCombatant, removeCombatant, editCombatant, setInitiative, setHp, adjustHp, } = useEncounter(); const activeCombatant = encounter.combatants[encounter.activeIndex]; const [nameInput, setNameInput] = useState(""); const handleAdd = (e: FormEvent) => { e.preventDefault(); if (nameInput.trim() === "") return; addCombatant(nameInput); setNameInput(""); }; return (

Initiative Tracker

{activeCombatant && (

Round {encounter.roundNumber} — Current: {activeCombatant.name}

)}
    {encounter.combatants.map((c, i) => (
  • {" "} { const raw = e.target.value; if (raw === "") { setInitiative(c.id, undefined); } else { const n = Number.parseInt(raw, 10); if (!Number.isNaN(n)) { setInitiative(c.id, n); } } }} />{" "} {c.maxHp !== undefined && c.currentHp !== undefined && ( )} { if (c.currentHp === undefined) return; const delta = value - c.currentHp; if (delta !== 0) adjustHp(c.id, delta); }} /> {c.maxHp !== undefined && /} {c.maxHp !== undefined && c.currentHp !== undefined && ( )}{" "} setHp(c.id, v)} />{" "}
  • ))}
setNameInput(e.target.value)} placeholder="Combatant name" />
{events.length > 0 && (

Events

    {events.map((e, i) => (
  • {formatEvent(e)}
  • ))}
)}
); }