generated from nhcarrigan/template
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
This commit is contained in:
@@ -41,7 +41,7 @@ const ToastItem = ({
|
||||
const crystals = achievement.reward?.crystals;
|
||||
|
||||
return (
|
||||
<div className="achievement-toast" onClick={handleClick}>
|
||||
<div className="game-toast" onClick={handleClick}>
|
||||
<span className="toast-icon">{achievement.icon}</span>
|
||||
<div className="toast-content">
|
||||
<span className="toast-label">{"Achievement Unlocked!"}</span>
|
||||
@@ -70,7 +70,7 @@ const AchievementToast = (): JSX.Element | null => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="achievement-toast-container">
|
||||
<>
|
||||
{pendingAchievements.map((achievement) => {
|
||||
return (
|
||||
<ToastItem
|
||||
@@ -80,7 +80,7 @@ const AchievementToast = (): JSX.Element | null => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const CodexToastItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="codex-toast" onClick={handleClick}>
|
||||
<div className="game-toast" onClick={handleClick}>
|
||||
<span className="toast-icon">{"📖"}</span>
|
||||
<div className="toast-content">
|
||||
<span className="toast-label">{"✨ Lore Unlocked!"}</span>
|
||||
@@ -70,13 +70,13 @@ const CodexToast = (): JSX.Element | null => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="achievement-toast-container">
|
||||
<>
|
||||
{pendingEntryIds.map((id) => {
|
||||
return (
|
||||
<CodexToastItem entryId={id} key={id} onDismiss={dismissCodexEntry} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ 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";
|
||||
@@ -164,9 +166,14 @@ const GameLayout = (): JSX.Element => {
|
||||
{schemaOutdated && !dismissedOutdatedWarning
|
||||
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
|
||||
: null}
|
||||
<AchievementToast />
|
||||
<CodexToast />
|
||||
<StoryToast />
|
||||
<div className="achievement-toast-container">
|
||||
<AchievementToast />
|
||||
<CodexToast />
|
||||
<MilestoneToast />
|
||||
<QuestCompleteToast />
|
||||
<QuestFailedToast />
|
||||
<StoryToast />
|
||||
</div>
|
||||
{loginBonus === null
|
||||
? null
|
||||
: <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @file Milestone toast notification component for prestige, transcendence, and apotheosis.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
/* eslint-disable react/no-multi-comp -- Sub-components are tightly coupled to their containers */
|
||||
import { type JSX, useEffect } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
|
||||
interface MilestoneToastItemProperties {
|
||||
readonly icon: string;
|
||||
readonly label: string;
|
||||
readonly onDismiss: ()=> void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single milestone toast notification.
|
||||
* @param props - The toast item properties.
|
||||
* @param props.icon - The emoji icon.
|
||||
* @param props.label - The label text.
|
||||
* @param props.onDismiss - Callback to dismiss the toast.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const MilestoneToastItem = ({
|
||||
icon,
|
||||
label,
|
||||
onDismiss,
|
||||
}: MilestoneToastItemProperties): JSX.Element => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onDismiss();
|
||||
}, 4000);
|
||||
return (): void => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [ onDismiss ]);
|
||||
|
||||
return (
|
||||
<div className="game-toast" onClick={onDismiss}>
|
||||
<span className="toast-icon">{icon}</span>
|
||||
<div className="toast-content">
|
||||
<span className="toast-label">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all milestone toasts (prestige, transcendence, apotheosis).
|
||||
* @returns The JSX element or null if no milestone toasts are pending.
|
||||
*/
|
||||
const MilestoneToast = (): JSX.Element | null => {
|
||||
const {
|
||||
showPrestigeToast,
|
||||
showTranscendenceToast,
|
||||
showApotheosisToast,
|
||||
dismissPrestigeToast,
|
||||
dismissTranscendenceToast,
|
||||
dismissApotheosisToast,
|
||||
} = useGame();
|
||||
|
||||
const hasAny
|
||||
= showPrestigeToast || showTranscendenceToast || showApotheosisToast;
|
||||
if (!hasAny) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showPrestigeToast
|
||||
? <MilestoneToastItem
|
||||
icon={"⭐"}
|
||||
label={"⭐ Prestige!"}
|
||||
onDismiss={dismissPrestigeToast}
|
||||
/>
|
||||
: null}
|
||||
{showTranscendenceToast
|
||||
? <MilestoneToastItem
|
||||
icon={"🌌"}
|
||||
label={"🌌 Transcendence!"}
|
||||
onDismiss={dismissTranscendenceToast}
|
||||
/>
|
||||
: null}
|
||||
{showApotheosisToast
|
||||
? <MilestoneToastItem
|
||||
icon={"✨"}
|
||||
label={"✨ Apotheosis!"}
|
||||
onDismiss={dismissApotheosisToast}
|
||||
/>
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { MilestoneToast };
|
||||
@@ -89,6 +89,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
enableNotifications,
|
||||
enableSounds,
|
||||
toggleAutoPrestige,
|
||||
triggerPrestigeToast,
|
||||
} = useGame();
|
||||
const [ isPending, setIsPending ] = useState(false);
|
||||
const [ result, setResult ] = useState<{
|
||||
@@ -128,6 +129,7 @@ const PrestigePanel = (): JSX.Element => {
|
||||
milestoneRunestones: data.milestoneRunestones,
|
||||
runestones: data.runestones,
|
||||
});
|
||||
triggerPrestigeToast();
|
||||
if (enableSounds) {
|
||||
playSound("prestige");
|
||||
}
|
||||
|
||||
@@ -190,10 +190,11 @@ const QuestPanel = (): JSX.Element => {
|
||||
}
|
||||
|
||||
const { adventurers, autoQuest, quests, zones } = state;
|
||||
// eslint-disable-next-line unicorn/no-array-reduce -- Need the total!
|
||||
const partyCombatPower = adventurers.reduce((total, adventurer) => {
|
||||
return total + adventurer.combatPower * adventurer.count;
|
||||
}, 0);
|
||||
let partyCombatPower = 0;
|
||||
for (const adventurer of adventurers) {
|
||||
const contribution = adventurer.combatPower * adventurer.count;
|
||||
partyCombatPower = partyCombatPower + contribution;
|
||||
}
|
||||
const zoneQuests = quests.filter(({ zoneId }) => {
|
||||
return zoneId === activeZoneId;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @file Quest toast notification component for completed and failed quests.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
/* eslint-disable react/no-multi-comp -- Sub-components are tightly coupled to their containers */
|
||||
import { type JSX, useEffect } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import type { Quest } from "@elysium/types";
|
||||
|
||||
interface QuestToastItemProperties {
|
||||
readonly quest: Quest;
|
||||
readonly onDismiss: (id: string)=> void;
|
||||
// eslint-disable-next-line react/require-default-props -- Default value set in destructuring
|
||||
readonly isFailure?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single quest toast notification.
|
||||
* @param props - The toast item properties.
|
||||
* @param props.quest - The quest to display.
|
||||
* @param props.onDismiss - Callback to dismiss the toast.
|
||||
* @param props.isFailure - Whether this is a failure toast.
|
||||
* @returns The JSX element.
|
||||
*/
|
||||
const QuestToastItem = ({
|
||||
quest,
|
||||
onDismiss,
|
||||
isFailure = false,
|
||||
}: QuestToastItemProperties): JSX.Element => {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
onDismiss(quest.id);
|
||||
}, 4000);
|
||||
return (): void => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [ quest.id, onDismiss ]);
|
||||
|
||||
function handleClick(): void {
|
||||
onDismiss(quest.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="game-toast" onClick={handleClick}>
|
||||
<span className="toast-icon">{isFailure
|
||||
? "💀"
|
||||
: "📜"}</span>
|
||||
<div className="toast-content">
|
||||
<span className="toast-label">{isFailure
|
||||
? "Quest Failed!"
|
||||
: "✨ Quest Complete!"}</span>
|
||||
<span className="toast-name">{quest.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the quest complete toast container.
|
||||
* @returns The JSX element or null if there are no pending quest toasts.
|
||||
*/
|
||||
const QuestCompleteToast = (): JSX.Element | null => {
|
||||
const { completedQuestToasts, dismissCompletedQuest } = useGame();
|
||||
|
||||
if (completedQuestToasts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{completedQuestToasts.map((quest) => {
|
||||
return (
|
||||
<QuestToastItem
|
||||
key={quest.id}
|
||||
onDismiss={dismissCompletedQuest}
|
||||
quest={quest}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the quest failed toast container.
|
||||
* @returns The JSX element or null if there are no pending failure toasts.
|
||||
*/
|
||||
const QuestFailedToast = (): JSX.Element | null => {
|
||||
const { failedQuestToasts, dismissFailedQuest } = useGame();
|
||||
|
||||
if (failedQuestToasts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{failedQuestToasts.map((quest) => {
|
||||
return (
|
||||
<QuestToastItem
|
||||
isFailure={true}
|
||||
key={quest.id}
|
||||
onDismiss={dismissFailedQuest}
|
||||
quest={quest}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { QuestCompleteToast, QuestFailedToast };
|
||||
@@ -45,13 +45,13 @@ const StoryToastItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="achievement-toast" onClick={handleClick} type="button">
|
||||
<span className="achievement-toast-icon">{"📖"}</span>
|
||||
<div className="achievement-toast-content">
|
||||
<span className="achievement-toast-label">{"✨ New Chapter!"}</span>
|
||||
<span className="achievement-toast-name">{chapter.title}</span>
|
||||
<div className="game-toast" onClick={handleClick}>
|
||||
<span className="toast-icon">{"📖"}</span>
|
||||
<div className="toast-content">
|
||||
<span className="toast-label">{"✨ New Chapter!"}</span>
|
||||
<span className="toast-name">{chapter.title}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -65,11 +65,11 @@ const StoryToast = (): JSX.Element | null => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="achievement-toast-container">
|
||||
<>
|
||||
{pendingChapterIds.map((id) => {
|
||||
return <StoryToastItem chapterId={id} key={id} />;
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user