009ea560f9
Reshape the lobby UI from a single Overview into a two-level sidebar (games · profile · DEV synthetic-reports) with four games sub-panels (active-past · recruitment · invitations · private-games). Move the `create new game` button into the private-games panel, merge the applications section into recruitment cards as status chips, and add DEV-only synthetic-report loader as a top-level screen. Add a paid-tier gate at backend `lobby.game.create`: free callers get `403 forbidden` before the lobby service is invoked. The UI hides the private-games sub-panel + create button on free tier (DEV affordances flag overrides). Update every integration test that creates a game to use a new `testenv.PromoteToPaid` helper; add a new `TestLobbyFlow_FreeUserCreateGameForbidden`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
4.8 KiB
Svelte
121 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
// Single-route screen dispatcher for the app-shell. There are no
|
|
// per-screen routes: the visible screen is selected from in-memory
|
|
// state (`session.status` for the auth gate, `appScreen.screen` for
|
|
// the authenticated screen) rather than from the URL. The root
|
|
// layout intercepts the `loading` and `unsupported` session states
|
|
// before this component renders, so here `session.status` is either
|
|
// `anonymous` (login) or `authenticated` (lobby / create / game).
|
|
import { onMount } from "svelte";
|
|
import { dev } from "$app/environment";
|
|
import { session } from "$lib/session-store.svelte";
|
|
import {
|
|
appScreen,
|
|
activeView,
|
|
type AppScreen,
|
|
type GameView,
|
|
type GameViewState,
|
|
} from "$lib/app-nav.svelte";
|
|
import LoginScreen from "$lib/screens/login-screen.svelte";
|
|
import LobbyScreen from "$lib/screens/lobby-screen.svelte";
|
|
import LobbyCreateScreen from "$lib/screens/lobby-create-screen.svelte";
|
|
import ProfileScreen from "$lib/screens/profile-screen.svelte";
|
|
import GameShell from "$lib/game/game-shell.svelte";
|
|
import GamesActivePastScreen from "$lib/screens/games-active-past-screen.svelte";
|
|
import GamesRecruitmentScreen from "$lib/screens/games-recruitment-screen.svelte";
|
|
import GamesInvitationsScreen from "$lib/screens/games-invitations-screen.svelte";
|
|
import GamesPrivateGamesScreen from "$lib/screens/games-private-games-screen.svelte";
|
|
import SyntheticReportsScreen from "$lib/screens/synthetic-reports-screen.svelte";
|
|
import { pushState } from "$app/navigation";
|
|
import { page } from "$app/state";
|
|
|
|
// Dev-only navigation affordance for the Playwright e2e suite. The
|
|
// single-URL app-shell has no per-screen / per-view routes, so a
|
|
// spec can no longer drive the UI by `page.goto("/games/:id/:view")`.
|
|
// Instead the suite seeds the session, loads `/` (which lands on the
|
|
// authenticated lobby), then calls `window.__galaxyNav.enterGame(...)`
|
|
// to switch the in-memory screen and view. Guarded by `dev` so it is
|
|
// stripped from the production bundle — `import.meta.env.DEV` (and the
|
|
// SvelteKit `dev` re-export) is statically `false` there, so the
|
|
// whole `onMount` body tree-shakes away.
|
|
type ViewParams = Omit<GameViewState, "view">;
|
|
interface NavSurface {
|
|
enterGame(gameId: string, view?: GameView, params?: ViewParams): void;
|
|
select(view: GameView, params?: ViewParams): void;
|
|
go(screen: AppScreen, opts?: { gameId?: string }): void;
|
|
}
|
|
type NavWindow = typeof globalThis & { __galaxyNav?: NavSurface };
|
|
|
|
onMount(() => {
|
|
if (!dev) return;
|
|
(window as NavWindow).__galaxyNav = {
|
|
enterGame(gameId, view = "map", params = {}): void {
|
|
activeView.select(view, params);
|
|
appScreen.go("game", { gameId });
|
|
},
|
|
select(view, params = {}): void {
|
|
activeView.select(view, params);
|
|
},
|
|
go(screen, opts = {}): void {
|
|
appScreen.go(screen, opts);
|
|
},
|
|
};
|
|
});
|
|
|
|
// Screen-level browser history (Back → lobby) without changing the URL.
|
|
// On the first authenticated render, stamp a restored overlay (game /
|
|
// lobby-create) on top of the load entry so Back falls through to lobby.
|
|
let historyStamped = $state(false);
|
|
$effect(() => {
|
|
if (session.status === "authenticated" && !historyStamped) {
|
|
historyStamped = true;
|
|
if (appScreen.screen === "game" && appScreen.gameId !== null) {
|
|
pushState("", { screen: "game", gameId: appScreen.gameId });
|
|
} else if (appScreen.screen === "lobby-create") {
|
|
pushState("", { screen: "lobby-create" });
|
|
} else if (appScreen.screen === "profile") {
|
|
pushState("", { screen: "profile" });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Sync the store from history on Back/Forward (popstate updates
|
|
// `page.state`). Skipped until the baseline is stamped so it never
|
|
// clobbers the restored screen on first render.
|
|
$effect(() => {
|
|
if (!historyStamped) return;
|
|
appScreen.syncFromHistory(page.state.screen, page.state.gameId ?? null);
|
|
});
|
|
</script>
|
|
|
|
{#if session.status === "authenticated"}
|
|
{#if appScreen.screen === "lobby-create"}
|
|
<LobbyCreateScreen />
|
|
{:else if appScreen.screen === "profile"}
|
|
<ProfileScreen />
|
|
{:else if appScreen.screen === "game" && appScreen.gameId !== null}
|
|
<GameShell />
|
|
{:else if appScreen.screen === "games-active-past"}
|
|
<GamesActivePastScreen />
|
|
{:else if appScreen.screen === "games-recruitment"}
|
|
<GamesRecruitmentScreen />
|
|
{:else if appScreen.screen === "games-invitations"}
|
|
<GamesInvitationsScreen />
|
|
{:else if appScreen.screen === "games-private-games"}
|
|
<GamesPrivateGamesScreen />
|
|
{:else if appScreen.screen === "synthetic-reports"}
|
|
<SyntheticReportsScreen />
|
|
{:else}
|
|
<!--
|
|
Default authenticated screen. Covers the historical `lobby`
|
|
alias and any restored snapshot that lost its game id. The
|
|
`LobbyScreen` resolver navigates to `games-recruitment` on
|
|
mount; the shell then re-routes to a more appropriate
|
|
sub-page if visibility rules allow.
|
|
-->
|
|
<LobbyScreen />
|
|
{/if}
|
|
{:else}
|
|
<LoginScreen />
|
|
{/if}
|