Files
galaxy-game/ui/frontend/src/lib/lobby-nav.ts
T
Ilia Denisov 6fbab5417f
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s
fix(ui): F8-04b lobby — auto-expand first available games sub + hide empty invitations
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>
2026-05-27 10:17:57 +02:00

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]!;
}