Files
elysium/apps/web/src/components/game/gameLayout.tsx
T
hikari d1d1f70c75 chore: fix lint, ensure full CI pipeline passes, add verify checklist
- Fix strict-boolean-expressions in 7 route files (runtime body validation)
- Fix no-unnecessary-condition in profile.ts and offlineProgress.ts (defensive null checks)
- Extend v8 ignore next-N counts in game.ts to reach 100% coverage
- Add CI requirements to CLAUDE.md (lint + build + test must pass before commit)
- Add manual verification checklist (verify.md)
- Remove progress.md
2026-03-08 13:59:38 -07:00

245 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Game layout component rendering the main game UI.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Complex layout with many conditional renders */
/* eslint-disable complexity -- Many tab render paths */
import { type JSX, useState } from "react";
import { useGame } from "../../context/gameContext.js";
import { ResourceBar } from "../ui/resourceBar.js";
import { AboutPanel } from "./aboutPanel.js";
import { AchievementPanel } from "./achievementPanel.js";
import { AchievementToast } from "./achievementToast.js";
import { AdventurerPanel } from "./adventurerPanel.js";
import { ApotheosisPanel } from "./apotheosisPanel.js";
import { BattleModal } from "./battleModal.js";
import { BossPanel } from "./bossPanel.js";
import { CharacterSheetPanel } from "./characterSheetPanel.js";
import { ClickArea } from "./clickArea.js";
import { CodexPanel } from "./codexPanel.js";
import { CodexToast } from "./codexToast.js";
import { CompanionPanel } from "./companionPanel.js";
import { CraftingPanel } from "./craftingPanel.js";
import { DailyChallengePanel } from "./dailyChallengePanel.js";
import { EditProfileModal } from "./editProfileModal.js";
import { EquipmentPanel } from "./equipmentPanel.js";
import { ExplorationPanel } from "./explorationPanel.js";
import { LoginBonusModal } from "./loginBonusModal.js";
import { OfflineModal } from "./offlineModal.js";
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
import { PrestigePanel } from "./prestigePanel.js";
import { QuestPanel } from "./questPanel.js";
import { StatisticsPanel } from "./statisticsPanel.js";
import { StoryPanel } from "./storyPanel.js";
import { StoryToast } from "./storyToast.js";
import { TranscendencePanel } from "./transcendencePanel.js";
import { UpgradePanel } from "./upgradePanel.js";
type Tab =
| "adventurers"
| "upgrades"
| "quests"
| "bosses"
| "equipment"
| "achievements"
| "prestige"
| "transcendence"
| "apotheosis"
| "statistics"
| "daily"
| "codex"
| "about"
| "exploration"
| "crafting"
| "character"
| "companions"
| "story";
const baseTabs: Array<{ id: Tab; label: string }> = [
{ id: "adventurers", label: "βš”οΈ Adventurers" },
{ id: "upgrades", label: "πŸ”§ Upgrades" },
{ id: "quests", label: "πŸ“œ Quests" },
{ id: "bosses", label: "πŸ‘Ή Bosses" },
{ id: "equipment", label: "πŸ—‘οΈ Equipment" },
{ id: "exploration", label: "πŸ—ΊοΈ Exploration" },
{ id: "crafting", label: "βš—οΈ Crafting" },
{ id: "daily", label: "πŸ“… Daily" },
{ id: "prestige", label: "⭐ Prestige" },
{ id: "transcendence", label: "🌌 Transcendence" },
{ id: "apotheosis", label: "✨ Apotheosis" },
{ id: "statistics", label: "πŸ“Š Statistics" },
{ id: "companions", label: "πŸ‘₯ Companions" },
{ id: "character", label: "πŸ“‹ Character" },
{ id: "achievements", label: "πŸ† Achievements" },
{ id: "story", label: "πŸ“– Story" },
{ id: "codex", label: "πŸ—ΊοΈ Codex" },
{ id: "about", label: "ℹ️ About" },
];
/**
* Renders the main game layout with tabs and panels.
* @returns The JSX element.
*/
const GameLayout = (): JSX.Element => {
const {
state,
isLoading,
error,
battleResult,
dismissBattle,
lastSavedAt,
isSyncing,
forceSync,
unlockedCodexEntryIds: pendingCodexEntryIds,
unlockedStoryChapterIds: pendingStoryChapterIds,
loginBonus,
dismissLoginBonus,
schemaOutdated,
} = useGame();
const [ activeTab, setActiveTab ] = useState<Tab>("adventurers");
const [ editingProfile, setEditingProfile ] = useState(false);
const [ dismissedOutdatedWarning, setDismissedOutdatedWarning ]
= useState(false);
if (isLoading) {
return (
<div className="loading-screen">
<p>{"Loading your adventure..."}</p>
</div>
);
}
if (error !== null && error !== "") {
return (
<div className="error-screen">
<p>
{"Error: "}
{error}
</p>
</div>
);
}
if (state === null) {
return (
<div className="loading-screen">
<p>{"Loading..."}</p>
</div>
);
}
const profileUrl = `/profile/${state.player.discordId}`;
const codexBadgeCount = pendingCodexEntryIds.length;
const storyBadgeCount = pendingStoryChapterIds.length;
function handleOpenEditProfile(): void {
setEditingProfile(true);
}
function handleCloseEditProfile(): void {
setEditingProfile(false);
}
function handleDismissOutdated(): void {
setDismissedOutdatedWarning(true);
}
return (
<div className="game-layout">
<ResourceBar
apotheosisCount={state.apotheosis?.count ?? 0}
isSyncing={isSyncing}
lastSavedAt={lastSavedAt}
onEditProfile={handleOpenEditProfile}
onForceSync={forceSync}
prestigeCount={state.prestige.count}
profileUrl={profileUrl}
resources={state.resources}
runestones={state.prestige.runestones}
transcendenceCount={state.transcendence?.count ?? 0}
/>
<OfflineModal />
{schemaOutdated && !dismissedOutdatedWarning
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
: null}
<AchievementToast />
<CodexToast />
<StoryToast />
{loginBonus === null
? null
: <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
}
{battleResult === null
? null
: <BattleModal battle={battleResult} onDismiss={dismissBattle} />
}
{editingProfile
? <EditProfileModal onClose={handleCloseEditProfile} />
: null}
<div className="game-main">
<aside className="game-sidebar">
<ClickArea />
<p className="game-copyright">{"Β© NHCarrigan"}</p>
</aside>
<main className="game-content">
<nav className="tab-bar">
{baseTabs.map((tab) => {
const { id: tabId, label } = tab;
function handleTabClick(): void {
setActiveTab(tabId);
}
return (
<button
className={`tab-button ${
activeTab === tabId
? "active"
: ""
}`}
key={tabId}
onClick={handleTabClick}
type="button"
>
{label}
{tabId === "codex" && codexBadgeCount > 0
&& <span className="tab-badge">{codexBadgeCount}</span>
}
{tabId === "story" && storyBadgeCount > 0
&& <span className="tab-badge">{storyBadgeCount}</span>
}
</button>
);
})}
</nav>
<div className="tab-content">
{activeTab === "adventurers" && <AdventurerPanel />}
{activeTab === "upgrades" && <UpgradePanel />}
{activeTab === "quests" && <QuestPanel />}
{activeTab === "bosses" && <BossPanel />}
{activeTab === "equipment" && <EquipmentPanel />}
{activeTab === "achievements" && <AchievementPanel />}
{activeTab === "prestige" && <PrestigePanel />}
{activeTab === "transcendence" && <TranscendencePanel />}
{activeTab === "apotheosis" && <ApotheosisPanel />}
{activeTab === "exploration" && <ExplorationPanel />}
{activeTab === "crafting" && <CraftingPanel />}
{activeTab === "statistics" && <StatisticsPanel />}
{activeTab === "daily" && <DailyChallengePanel />}
{activeTab === "companions" && <CompanionPanel />}
{activeTab === "character" && <CharacterSheetPanel />}
{activeTab === "story" && <StoryPanel />}
{activeTab === "codex" && <CodexPanel />}
{activeTab === "about" && <AboutPanel />}
</div>
</main>
</div>
</div>
);
};
export { GameLayout };