Files
initiative/apps/web/src/App.tsx
Lukas 86768842ff
All checks were successful
CI / check (push) Successful in 1m18s
CI / build-image (push) Has been skipped
Refactor App.tsx from god component to context-based architecture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:33:33 +01:00

127 lines
3.8 KiB
TypeScript

import { useEffect, useRef } from "react";
import { ActionBar } from "./components/action-bar.js";
import { BulkImportToasts } from "./components/bulk-import-toasts.js";
import { CombatantRow } from "./components/combatant-row.js";
import {
PlayerCharacterSection,
type PlayerCharacterSectionHandle,
} from "./components/player-character-section.js";
import { StatBlockPanel } from "./components/stat-block-panel.js";
import { Toast } from "./components/toast.js";
import { TurnNavigation } from "./components/turn-navigation.js";
import { useEncounterContext } from "./contexts/encounter-context.js";
import { useInitiativeRollsContext } from "./contexts/initiative-rolls-context.js";
import { useSidePanelContext } from "./contexts/side-panel-context.js";
import { useActionBarAnimation } from "./hooks/use-action-bar-animation.js";
import { useAutoStatBlock } from "./hooks/use-auto-stat-block.js";
import { cn } from "./lib/utils.js";
export function App() {
const { encounter, isEmpty } = useEncounterContext();
const sidePanel = useSidePanelContext();
const rolls = useInitiativeRollsContext();
useAutoStatBlock();
const playerCharacterRef = useRef<PlayerCharacterSectionHandle>(null);
const actionBarInputRef = useRef<HTMLInputElement>(null);
const activeRowRef = useRef<HTMLDivElement>(null);
const actionBarAnim = useActionBarAnimation(encounter.combatants.length);
// Auto-scroll to active combatant when turn changes
const activeIndex = encounter.activeIndex;
useEffect(() => {
if (activeIndex >= 0) {
activeRowRef.current?.scrollIntoView({
block: "nearest",
behavior: "smooth",
});
}
}, [activeIndex]);
return (
<div className="flex h-dvh flex-col">
<div className="relative mx-auto flex min-h-0 w-full max-w-2xl flex-1 flex-col gap-3 px-4">
{!!actionBarAnim.showTopBar && (
<div
className={cn("shrink-0 pt-8", actionBarAnim.topBarClass)}
onAnimationEnd={actionBarAnim.onTopBarExitEnd}
>
<TurnNavigation />
</div>
)}
{isEmpty ? (
<div className="flex min-h-0 flex-1 items-center justify-center pt-8 pb-[15%]">
<div
className={cn("w-full", actionBarAnim.risingClass)}
onAnimationEnd={actionBarAnim.onRiseEnd}
>
<ActionBar
inputRef={actionBarInputRef}
onManagePlayers={() =>
playerCharacterRef.current?.openManagement()
}
autoFocus
/>
</div>
</div>
) : (
<>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="flex flex-col px-2 py-2">
{encounter.combatants.map((c, i) => (
<CombatantRow
key={c.id}
ref={i === encounter.activeIndex ? activeRowRef : null}
combatant={c}
isActive={i === encounter.activeIndex}
/>
))}
</div>
</div>
<div
className={cn("shrink-0 pb-8", actionBarAnim.settlingClass)}
onAnimationEnd={actionBarAnim.onSettleEnd}
>
<ActionBar
inputRef={actionBarInputRef}
onManagePlayers={() =>
playerCharacterRef.current?.openManagement()
}
/>
</div>
</>
)}
</div>
{!!sidePanel.pinnedCreatureId && sidePanel.isWideDesktop && (
<StatBlockPanel panelRole="pinned" side="left" />
)}
<StatBlockPanel panelRole="browse" side="right" />
<BulkImportToasts />
{rolls.rollSkippedCount > 0 && (
<Toast
message={`${rolls.rollSkippedCount} skipped — bestiary source not loaded`}
onDismiss={rolls.dismissRollSkipped}
autoDismissMs={4000}
/>
)}
{!!rolls.rollSingleSkipped && (
<Toast
message="Can't roll — bestiary source not loaded"
onDismiss={rolls.dismissRollSingleSkipped}
autoDismissMs={4000}
/>
)}
<PlayerCharacterSection ref={playerCharacterRef} />
</div>
);
}