diff --git a/src/client/App.tsx b/src/client/App.tsx index 9983d96..220012e 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -5,6 +5,7 @@ import { EventDetail } from "./components/EventDetail.tsx"; import { EventRow, type RowEvent } from "./components/EventRow.tsx"; import { NextUp } from "./components/NextUp.tsx"; import { Timeline } from "./components/Timeline.tsx"; +import { Welcome } from "./components/Welcome.tsx"; import { useCompletions } from "./state/useCompletions.ts"; import { usePrefs } from "./state/usePrefs.ts"; import { clockFor, DAY, endingSoonestFirst, formatRemaining } from "../shared/time.ts"; @@ -91,6 +92,25 @@ export function App() { ); } + // First run: ask which games before showing a calendar full of ones they + // don't play. Stored as hiddenGames (the inverse) so a game added later shows + // up by default rather than staying invisible. + if (!prefs.onboarded) { + return ( + + + update({ + onboarded: true, + hiddenGames: games.filter((g) => !chosen.includes(g)), + }) + } + /> + + ); + } + const staleSources = state.feed.sources.filter( (s) => s.lastSuccessAt === null || now - Date.parse(s.lastSuccessAt) > 2 * DAY, ); diff --git a/src/client/components/Welcome.tsx b/src/client/components/Welcome.tsx new file mode 100644 index 0000000..7b9a17d --- /dev/null +++ b/src/client/components/Welcome.tsx @@ -0,0 +1,118 @@ +import { useState } from "react"; +import { gameMeta } from "../../shared/games.ts"; +import type { GameId } from "../../shared/schema.ts"; + +/** + * First run: pick your games. + * + * Asked once, before any events are shown, because a calendar full of games you + * don't play is worse than an empty one — it buries the thing you came for. + * + * Nothing is preselected. An empty state with a disabled button is clearer than + * guessing on the reader's behalf and hoping they notice. + */ +export function Welcome({ + available, + onConfirm, +}: { + available: GameId[]; + onConfirm: (chosen: GameId[]) => void; +}) { + const [chosen, setChosen] = useState([]); + + const toggle = (id: GameId) => + setChosen((prev) => + prev.includes(id) ? prev.filter((g) => g !== id) : [...prev, id], + ); + + return ( +
+

+ EVENTCLOCK +

+ +

+ Which games do you play? +

+

+ You'll only see events for these. Change it any time — nothing is saved + anywhere but this browser. +

+ +
+ {available.map((id) => { + const game = gameMeta(id); + const on = chosen.includes(id); + return ( + + ); + })} +
+ +
+ + +
+ +

+ More games are coming as sources are added. Anything you switch on later + shows up automatically. +

+
+ ); +} diff --git a/src/client/state/usePrefs.ts b/src/client/state/usePrefs.ts index b471555..8332cf9 100644 --- a/src/client/state/usePrefs.ts +++ b/src/client/state/usePrefs.ts @@ -10,6 +10,8 @@ export interface Prefs { showCompleted: boolean; /** False until the reader confirms or changes the guessed region. */ regionConfirmed: boolean; + /** False until the reader has picked their games on first run. */ + onboarded: boolean; } function defaults(): Prefs { @@ -18,6 +20,7 @@ function defaults(): Prefs { hiddenGames: [], showCompleted: true, regionConfirmed: false, + onboarded: false, }; }