Files
elysium/apps/web/src/components/game/gameLayout.tsx
T
hikari f9c925b9fc
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m3s
CI / Lint, Build & Test (push) Successful in 1m5s
feat: unify toast styles and add quest/milestone toast notifications
- Merge .codex-toast and .achievement-toast into a single .game-toast class
- Fix storyToast inner class names and replace <button> wrapper with <div>
- Add QuestCompleteToast and QuestFailedToast components
- Add MilestoneToast for prestige, transcendence, and apotheosis events
- Move shared toast container to gameLayout so all toasts stack in one column
- Wire quest detection in GameContext to store full Quest objects for toast names
- Trigger prestige toast from both auto-prestige and manual prestige panel
2026-03-08 18:47:42 -07:00

252 lines
8.2 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 { MilestoneToast } from "./milestoneToast.js";
import { OfflineModal } from "./offlineModal.js";
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
import { PrestigePanel } from "./prestigePanel.js";
import { QuestPanel } from "./questPanel.js";
import { QuestCompleteToast, QuestFailedToast } from "./questToast.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}
<div className="achievement-toast-container">
<AchievementToast />
<CodexToast />
<MilestoneToast />
<QuestCompleteToast />
<QuestFailedToast />
<StoryToast />
</div>
{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 };