Files
gacha-event-tracker/src/client/state/reader/onboarding.ts
T
Lucas WintherandClaude Opus 5.5 50c6e91b00 feat: the redesigned Event Clock
The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.

Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.

Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 01:55:16 +02:00

94 lines
2.8 KiB
TypeScript

import type { Region } from "../../../shared/schema.ts";
import type { View } from "../usePrefs.ts";
import { routinesFor, offered } from "../../../shared/routines.ts";
import { createAccount, changeAccountRegion } from "./accounts.ts";
import type { AccountId, ReaderState, TrackingIntent } from "./schema.ts";
export interface SetupDraft {
region: Region;
interface: "relaxed" | "structured";
theme: "dark" | "light" | "system";
scheme: string;
timezone: string | null;
games: string[];
tracking: Record<string, TrackingIntent>;
showUpcoming: boolean;
view: View;
/** Asked only when the reader picks Timeline; absent keeps the default look. */
timelineLook?: "new" | "classic";
}
export function setupReader(
draft: SetupDraft,
existing: ReaderState,
at: string,
mint: () => AccountId,
): ReaderState {
const accounts = { ...existing.accounts };
for (const game of draft.games) {
let a =
Object.values(accounts).find((a) => a.game === game && !a.retiredAt) ??
createAccount(game, draft.region, at, mint());
if (a.region !== draft.region)
a = changeAccountRegion(a, draft.region, at, `setup-${at}`);
const intent = draft.tracking[game] ?? {
events: true,
dailies: false,
weeklyMonthly: false,
cycles: false,
};
const routines = {
...a.routines,
[`dailies:${game}`]: {
followed: intent.dailies,
checklist: intent.dailies,
timeline: intent.dailies,
},
};
for (const r of routinesFor(game)) {
const followed =
offered(r, existing.prefs.standardPulls === true) &&
(r.period.unit === "cycle" ? intent.cycles : intent.weeklyMonthly);
routines[r.id] = { followed, checklist: followed, timeline: followed };
}
accounts[a.id] = { ...a, intent, routines };
}
for (const [id, a] of Object.entries(accounts))
if (!draft.games.includes(a.game))
accounts[id] = {
...a,
intent: {
events: false,
dailies: false,
weeklyMonthly: false,
cycles: false,
},
routines: Object.fromEntries(
Object.entries(a.routines).map(([id, p]) => [
id,
{ ...p, followed: false },
]),
),
};
return {
...existing,
accounts,
prefs: {
...existing.prefs,
region: draft.region,
interface: draft.interface,
theme: draft.theme,
scheme: draft.scheme,
timezone: draft.timezone,
showUpcoming: draft.showUpcoming,
view: draft.view,
...(draft.view === "timeline" && draft.timelineLook
? { timelineLook: draft.timelineLook }
: {}),
onboarded: true,
redesignTourSeen: "2026-10-redesign",
hiddenGames: Object.values(accounts)
.map((a) => a.game)
.filter((g) => !draft.games.includes(g)),
},
};
}