6fbab5417f
Two follow-up nits on the F8-04b sidebar: 1. The bare-`lobby` resolver (lobby-screen.svelte) redirected to `games-recruitment` unconditionally on mount. With games already in the player's roster the sidebar then highlighted the wrong sub-page. The resolver now awaits the lobby fan-out + account fetch, then hands off to the same `firstVisibleGamesScreen` helper the sidebar uses — so a fresh entry with games lands on `active-past`, the canonical-order fallback stays `recruitment`. 2. `games-invitations` was unconditionally visible in the sidebar. Now it follows the `active-past` rule: hidden until the pending-invites list reports >=1. The lobby shell's auto-kick effect treats it symmetrically — accepting / declining the last invite moves the player to the next visible sub-page once the fan-out has resolved. Acceptance order in games-invitations-screen.acceptInvite was also swapped to setMyGames-before-removeInvitation: both mutations land in the same microtask, so the new auto-kick sees the freshly added game in `myGames` when invitations drop to zero and routes the player to `active-past` instead of bouncing through `recruitment`. The visibility predicates and canonical order live in the new `src/lib/lobby-nav.ts` pure helper, shared between the sidebar and the resolver so they cannot disagree. Unit tests cover every combination of (hasMyGames, hasInvitations, isPaidOrDev). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
// Pure helpers for the lobby `games` submenu.
|
|
//
|
|
// Both the sidebar (`lib/screens/lobby-shell.svelte`) and the bare-`lobby`
|
|
// resolver (`lib/screens/lobby-screen.svelte`) need to agree on which
|
|
// sub-screen is the "first available" — the one auto-expanded in the
|
|
// sidebar and the redirect target when the user lands on the bare lobby
|
|
// alias. Centralising the predicates here keeps the two surfaces from
|
|
// drifting apart.
|
|
//
|
|
// The functions are intentionally pure: they take a resolved snapshot of
|
|
// lobby / account state, return the visible sub-screen list (in canonical
|
|
// order) and the first entry, and never touch stores or globals. Callers
|
|
// derive the state from `lobbyData` / `account` and the DEV-affordances
|
|
// flag.
|
|
|
|
export type GamesSubScreen =
|
|
| "games-active-past"
|
|
| "games-recruitment"
|
|
| "games-invitations"
|
|
| "games-private-games";
|
|
|
|
/**
|
|
* LobbyNavState is the resolved snapshot of the lobby state the
|
|
* games-submenu visibility rules depend on. Build it from the live
|
|
* stores at the call site; the helpers below stay pure on top.
|
|
*/
|
|
export interface LobbyNavState {
|
|
/** Player has at least one own / past game in `lobby.my-games.list`. */
|
|
readonly hasMyGames: boolean;
|
|
/** Player has at least one pending invite in `lobby.invites.list`. */
|
|
readonly hasInvitations: boolean;
|
|
/** Player is on a paid tier, or the build exposes DEV affordances. */
|
|
readonly isPaidOrDev: boolean;
|
|
}
|
|
|
|
/**
|
|
* visibleGamesSubScreens returns the games-submenu sub-screens that
|
|
* are currently visible, in canonical display order:
|
|
*
|
|
* 1. `games-active-past` — only when the player has games.
|
|
* 2. `games-recruitment` — always visible.
|
|
* 3. `games-invitations` — only when the player has pending invites.
|
|
* 4. `games-private-games` — only when paid-tier or DEV.
|
|
*
|
|
* `games-recruitment` is unconditional, so the returned array is
|
|
* always non-empty.
|
|
*/
|
|
export function visibleGamesSubScreens(
|
|
state: LobbyNavState,
|
|
): readonly GamesSubScreen[] {
|
|
const out: GamesSubScreen[] = [];
|
|
if (state.hasMyGames) out.push("games-active-past");
|
|
out.push("games-recruitment");
|
|
if (state.hasInvitations) out.push("games-invitations");
|
|
if (state.isPaidOrDev) out.push("games-private-games");
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* firstVisibleGamesScreen returns the first entry from
|
|
* `visibleGamesSubScreens(state)`. Since `games-recruitment` is
|
|
* unconditional, the result is always defined.
|
|
*/
|
|
export function firstVisibleGamesScreen(state: LobbyNavState): GamesSubScreen {
|
|
return visibleGamesSubScreens(state)[0]!;
|
|
}
|