generated from nhcarrigan/template
fix: persist UI preferences across navigation and sessions (#48)
## Summary - **#35** — Adventure multiplier selection is now persisted in `localStorage` (`"elysium_batch_size"`). The chosen batch size is restored automatically on the next visit, with a graceful fallback to `1` for missing or unrecognisable values. - **#36** — Zone selection in the boss panel and quest panel is now persisted in `sessionStorage` (`"elysium_boss_zone"` / `"elysium_quest_zone"`). The selected zone survives navigation within a session and resets cleanly when the session ends, defaulting to Verdant Vale if no stored value exists. ## Test plan - [x] Lint — zero errors, zero warnings - [x] Build — all packages build cleanly - [x] Tests — 415 tests passing, 100% coverage across all packages - [ ] Manual: select a non-default batch size, refresh the page — multiplier should be restored - [ ] Manual: switch to a non-default zone in the boss panel, navigate away and back — zone should still be selected - [ ] Manual: repeat for the quest panel - [ ] Manual: log out and back in — zone selection should reset to Verdant Vale ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #48 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #48.
This commit is contained in:
@@ -16,6 +16,31 @@ import type { Adventurer } from "@elysium/types";
|
|||||||
type BatchSize = 1 | 5 | 10 | 25 | 100 | "max";
|
type BatchSize = 1 | 5 | 10 | 25 | 100 | "max";
|
||||||
const batchOptions: Array<BatchSize> = [ 1, 5, 10, 25, 100, "max" ];
|
const batchOptions: Array<BatchSize> = [ 1, 5, 10, 25, 100, "max" ];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a localStorage string back into a valid BatchSize, defaulting to 1.
|
||||||
|
* @param stored - The raw string from localStorage (or null if absent).
|
||||||
|
* @returns A valid BatchSize value.
|
||||||
|
*/
|
||||||
|
const parseBatchSize = (stored: string | null): BatchSize => {
|
||||||
|
if (stored === "max") {
|
||||||
|
return "max";
|
||||||
|
}
|
||||||
|
const numeric = Number(stored);
|
||||||
|
if (numeric === 5) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
if (numeric === 10) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
if (numeric === 25) {
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
if (numeric === 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the total cost to buy a batch of adventurers.
|
* Computes the total cost to buy a batch of adventurers.
|
||||||
* @param adventurer - The adventurer to buy.
|
* @param adventurer - The adventurer to buy.
|
||||||
@@ -148,7 +173,9 @@ const AdventurerCard = ({
|
|||||||
const AdventurerPanel = (): JSX.Element => {
|
const AdventurerPanel = (): JSX.Element => {
|
||||||
const { state, formatNumber } = useGame();
|
const { state, formatNumber } = useGame();
|
||||||
const [ showLocked, setShowLocked ] = useState(true);
|
const [ showLocked, setShowLocked ] = useState(true);
|
||||||
const [ batchSize, setBatchSize ] = useState<BatchSize>(1);
|
const [ batchSize, setBatchSize ] = useState<BatchSize>(() => {
|
||||||
|
return parseBatchSize(localStorage.getItem("elysium_batch_size"));
|
||||||
|
});
|
||||||
|
|
||||||
if (state === null) {
|
if (state === null) {
|
||||||
return (
|
return (
|
||||||
@@ -196,6 +223,7 @@ const AdventurerPanel = (): JSX.Element => {
|
|||||||
{batchOptions.map((option) => {
|
{batchOptions.map((option) => {
|
||||||
function handleBatchSelect(): void {
|
function handleBatchSelect(): void {
|
||||||
setBatchSize(option);
|
setBatchSize(option);
|
||||||
|
localStorage.setItem("elysium_batch_size", String(option));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -239,7 +239,9 @@ const BossPanel = (): JSX.Element => {
|
|||||||
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
|
const [ activeZoneId, setActiveZoneId ] = useState(() => {
|
||||||
|
return sessionStorage.getItem("elysium_boss_zone") ?? "verdant_vale";
|
||||||
|
});
|
||||||
const [ showLocked, setShowLocked ] = useState(true);
|
const [ showLocked, setShowLocked ] = useState(true);
|
||||||
|
|
||||||
if (state === null) {
|
if (state === null) {
|
||||||
@@ -317,6 +319,11 @@ const BossPanel = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleZoneSelect(zoneId: string): void {
|
||||||
|
setActiveZoneId(zoneId);
|
||||||
|
sessionStorage.setItem("elysium_boss_zone", zoneId);
|
||||||
|
}
|
||||||
|
|
||||||
function handleToggle(): void {
|
function handleToggle(): void {
|
||||||
setShowLocked((current) => {
|
setShowLocked((current) => {
|
||||||
return !current;
|
return !current;
|
||||||
@@ -374,7 +381,7 @@ const BossPanel = (): JSX.Element => {
|
|||||||
|
|
||||||
<ZoneSelector
|
<ZoneSelector
|
||||||
activeZoneId={activeZoneId}
|
activeZoneId={activeZoneId}
|
||||||
onSelectZone={setActiveZoneId}
|
onSelectZone={handleZoneSelect}
|
||||||
zones={zones}
|
zones={zones}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ const CharacterPage = ({ discordId }: CharacterPageProperties): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="character-page-equipment-item"
|
className="character-page-equipment-item"
|
||||||
key={item.type}
|
key={item.name}
|
||||||
>
|
>
|
||||||
<div className="character-page-equipment-header">
|
<div className="character-page-equipment-header">
|
||||||
<span className="character-page-equipment-slot">
|
<span className="character-page-equipment-slot">
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ interface CollectResult {
|
|||||||
const ExplorationPanel = (): JSX.Element => {
|
const ExplorationPanel = (): JSX.Element => {
|
||||||
const { state, startExploration, collectExploration, formatNumber }
|
const { state, startExploration, collectExploration, formatNumber }
|
||||||
= useGame();
|
= useGame();
|
||||||
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
|
const [ activeZoneId, setActiveZoneId ] = useState(() => {
|
||||||
|
return sessionStorage.getItem("elysium_explore_zone") ?? "verdant_vale";
|
||||||
|
});
|
||||||
const [ pendingAreaId, setPendingAreaId ] = useState<string | null>(null);
|
const [ pendingAreaId, setPendingAreaId ] = useState<string | null>(null);
|
||||||
const [ lastResult, setLastResult ] = useState<CollectResult | null>(null);
|
const [ lastResult, setLastResult ] = useState<CollectResult | null>(null);
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ const ExplorationPanel = (): JSX.Element => {
|
|||||||
function handleZoneSelect(id: string): void {
|
function handleZoneSelect(id: string): void {
|
||||||
setActiveZoneId(id);
|
setActiveZoneId(id);
|
||||||
setLastResult(null);
|
setLastResult(null);
|
||||||
|
sessionStorage.setItem("elysium_explore_zone", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const goldChange = lastResult?.response.event?.goldChange ?? 0;
|
const goldChange = lastResult?.response.event?.goldChange ?? 0;
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ const QuestCard = ({
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<div className="quest-rewards">
|
<div className="quest-rewards">
|
||||||
{quest.rewards.map((reward) => {
|
{quest.rewards.map((reward, rewardIndex) => {
|
||||||
return (
|
return (
|
||||||
<span className="reward-tag" key={`${reward.type}-${String(reward.amount ?? "")}`}>
|
<span className="reward-tag" key={`${reward.type}-${reward.targetId ?? String(reward.amount ?? rewardIndex)}`}>
|
||||||
{reward.type === "gold"
|
{reward.type === "gold"
|
||||||
&& `🪙 ${formatNumber(reward.amount ?? 0)}`}
|
&& `🪙 ${formatNumber(reward.amount ?? 0)}`}
|
||||||
{reward.type === "essence"
|
{reward.type === "essence"
|
||||||
@@ -184,7 +184,9 @@ const QuestCard = ({
|
|||||||
*/
|
*/
|
||||||
const QuestPanel = (): JSX.Element => {
|
const QuestPanel = (): JSX.Element => {
|
||||||
const { state, toggleAutoQuest } = useGame();
|
const { state, toggleAutoQuest } = useGame();
|
||||||
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
|
const [ activeZoneId, setActiveZoneId ] = useState(() => {
|
||||||
|
return sessionStorage.getItem("elysium_quest_zone") ?? "verdant_vale";
|
||||||
|
});
|
||||||
const [ showLocked, setShowLocked ] = useState(true);
|
const [ showLocked, setShowLocked ] = useState(true);
|
||||||
|
|
||||||
if (state === null) {
|
if (state === null) {
|
||||||
@@ -243,6 +245,11 @@ const QuestPanel = (): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleZoneSelect(zoneId: string): void {
|
||||||
|
setActiveZoneId(zoneId);
|
||||||
|
sessionStorage.setItem("elysium_quest_zone", zoneId);
|
||||||
|
}
|
||||||
|
|
||||||
function handleToggle(): void {
|
function handleToggle(): void {
|
||||||
setShowLocked((current) => {
|
setShowLocked((current) => {
|
||||||
return !current;
|
return !current;
|
||||||
@@ -285,7 +292,7 @@ const QuestPanel = (): JSX.Element => {
|
|||||||
|
|
||||||
<ZoneSelector
|
<ZoneSelector
|
||||||
activeZoneId={activeZoneId}
|
activeZoneId={activeZoneId}
|
||||||
onSelectZone={setActiveZoneId}
|
onSelectZone={handleZoneSelect}
|
||||||
zones={zones}
|
zones={zones}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user