feat(settings): keep the game tab when switching games

Codes in one game now opens Codes in the next. The reader app holds the
tab and GameSettings reads it, instead of each game page minting its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-20 03:39:52 +02:00
1 parent 94a2faa6f6
commit 83a255754c
4 files changed
+43 -10

No files matched your search

+2 -1
View File
@@ -637,7 +637,8 @@ legend on the same baseline as "Next: …".
- **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance - **Settings.** About, Changelog, Data & sources (list options first), Region & time, Appearance
(interface, theme, scheme, urgency colours, default view, game order, timeline design, focus bar, (interface, theme, scheme, urgency colours, default view, game order, timeline design, focus bar,
checklist group), Your own events and games, Backup (tiers plus a manual "Your backups" list), and checklist group), Your own events and games, Backup (tiers plus a manual "Your backups" list), and
per-game Setup / Codes / About. per-game Setup / Codes / About. Switching to another game keeps the open tab (2026-10-20, owner):
Codes in one game opens Codes in the next.
- **Redeem codes (2026-10-18, owner).** A game's Codes tab lists the codes still to use first. A - **Redeem codes (2026-10-18, owner).** A game's Codes tab lists the codes still to use first. A
code redeemed on every server the reader plays, or past its stated expiry, is greyed and struck code redeemed on every server the reader plays, or past its stated expiry, is greyed and struck
through in a "Redeemed or expired" section below, with its Redeemed box still there to undo. A through in a "Redeemed or expired" section below, with its Redeemed box still there to undo. A
+9 -5
View File
@@ -88,7 +88,10 @@ import {
type SettingsPage, type SettingsPage,
} from "./components/redesign/SettingsLayout.tsx"; } from "./components/redesign/SettingsLayout.tsx";
import { ChangelogPage } from "./components/redesign/ChangelogPage.tsx"; import { ChangelogPage } from "./components/redesign/ChangelogPage.tsx";
import { GameSettings } from "./components/redesign/GameSettings.tsx"; import {
GameSettings,
type GameTab,
} from "./components/redesign/GameSettings.tsx";
import { RoutinesPage } from "./components/redesign/RoutinesPage.tsx"; import { RoutinesPage } from "./components/redesign/RoutinesPage.tsx";
import { DailyPanel } from "./components/redesign/DailyPanel.tsx"; import { DailyPanel } from "./components/redesign/DailyPanel.tsx";
import { RoutineGroup } from "./components/redesign/RoutineGroup.tsx"; import { RoutineGroup } from "./components/redesign/RoutineGroup.tsx";
@@ -292,7 +295,7 @@ function ReaderContent({
}, [tour]); }, [tour]);
const touring = tour === "new" || tour === "returning" || tour === "replay"; const touring = tour === "new" || tour === "returning" || tour === "replay";
const tourForced = tour === "new" || tour === "returning"; const tourForced = tour === "new" || tour === "returning";
const [gameTab, setGameTab] = useState<"setup" | "codes">("setup"); const [gameTab, setGameTab] = useState<GameTab>("setup");
const [codesGame, setCodesGame] = useState<string | undefined>( const [codesGame, setCodesGame] = useState<string | undefined>(
startRoute?.game, startRoute?.game,
); );
@@ -1021,8 +1024,8 @@ function ReaderContent({
setSettingsPage(p); setSettingsPage(p);
}} }}
onGame={(g) => { onGame={(g) => {
// The tab stays: Codes in one game opens Codes in the next.
setCodesGame(g); setCodesGame(g);
setGameTab("setup");
setSettingsPage("games"); setSettingsPage("games");
}} }}
> >
@@ -1041,8 +1044,9 @@ function ReaderContent({
/> />
) : settingsPage === "games" && codesGame ? ( ) : settingsPage === "games" && codesGame ? (
<GameSettings <GameSettings
key={`${codesGame}-${gameTab}`} key={codesGame}
initialTab={gameTab} tab={gameTab}
onTab={setGameTab}
codes={codes} codes={codes}
initialGame={codesGame} initialGame={codesGame}
state={state} state={state}
@@ -29,6 +29,8 @@ import { DisplayTimeContext, formatInZone } from "../../state/displayTime.ts";
import { resolveReaderClock } from "../../../shared/readerClock.ts"; import { resolveReaderClock } from "../../../shared/readerClock.ts";
import { DAY, formatRemaining, resetShiftMs } from "../../../shared/time.ts"; import { DAY, formatRemaining, resetShiftMs } from "../../../shared/time.ts";
export type ActionHandler = (action: ReaderAction) => Promise<SaveResult>; export type ActionHandler = (action: ReaderAction) => Promise<SaveResult>;
export const GAME_TABS = ["setup", "codes", "about"] as const;
export type GameTab = (typeof GAME_TABS)[number];
const hostOf = (url: string) => { const hostOf = (url: string) => {
try { try {
return new URL(url).hostname.replace(/^www\./, ""); return new URL(url).hostname.replace(/^www\./, "");
@@ -42,14 +44,17 @@ export function GameSettings({
onAction, onAction,
codes = null, codes = null,
initialGame, initialGame,
initialTab = "setup", tab: heldTab,
onTab,
}: { }: {
state: ReaderState; state: ReaderState;
sources: SourceHealth[]; sources: SourceHealth[];
onAction: ActionHandler; onAction: ActionHandler;
codes?: CodesFeed | null; codes?: CodesFeed | null;
initialGame?: string | undefined; initialGame?: string | undefined;
initialTab?: "setup" | "codes"; // When the parent holds the tab, switching games keeps the reader on it.
tab?: GameTab;
onTab?: (tab: GameTab) => void;
}) { }) {
const renderedMeta = useGameMeta(); const renderedMeta = useGameMeta();
const games = orderGames( const games = orderGames(
@@ -72,7 +77,9 @@ export function GameSettings({
games[0] ?? games[0] ??
"", "",
); );
const [tab, setTab] = useState<string>(initialTab); const [ownTab, setOwnTab] = useState<GameTab>("setup");
const tab = heldTab ?? ownTab,
setTab = onTab ?? setOwnTab;
const [trackingAccount, setTrackingAccount] = useState<string | null>(null); const [trackingAccount, setTrackingAccount] = useState<string | null>(null);
const [addingServer, setAddingServer] = useState(false); const [addingServer, setAddingServer] = useState(false);
const [server, setServer] = useState<Region>("asia"); const [server, setServer] = useState<Region>("asia");
@@ -138,7 +145,7 @@ export function GameSettings({
) : null} ) : null}
<div className="subtabs"> <div className="subtabs">
{["setup", "codes", "about"].map((id) => ( {GAME_TABS.map((id) => (
<button <button
type="button" type="button"
key={id} key={id}
+21
View File
@@ -58,3 +58,24 @@ test("Appearance orders only the games being tracked, with up and down buttons",
expect(html).not.toContain("Move Zenless Zone Zero"); expect(html).not.toContain("Move Zenless Zone Zero");
expect(html).not.toContain("Sort A–Z"); expect(html).not.toContain("Sort A–Z");
}); });
test("game settings shows the tab the parent holds, so switching games keeps it", () => {
const at = "2026-10-16T12:00:00.000Z",
ids = ["account:one", "account:two"],
state = migrateLegacy({}, ["genshin", "zzz"], at, () => ids.shift()!);
const html = (game: string) =>
render(
<GameSettings
state={state}
initialGame={game}
tab="codes"
onTab={() => {}}
sources={[]}
onAction={async () => ({ ok: true })}
/>,
);
for (const game of ["genshin", "zzz"]) {
expect(html(game)).toContain('data-tab="codes" aria-pressed="true"');
expect(html(game)).toContain("Redeem codes");
}
});