Files
galaxy-game/ui/frontend/src/lib/screens/games-invitations-screen.svelte
T
Ilia Denisov 009ea560f9
Tests · Go / test (push) Successful in 2m17s
Tests · UI / test (push) Waiting to run
feat(lobby): F8-04b hierarchical sidebar + paid-tier gate for create-game
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>
2026-05-26 23:53:53 +02:00

128 lines
3.8 KiB
Svelte

<!--
Pending invitations panel for the lobby `games` section. Surfaces
user-bound invites that have not been redeemed or declined yet, with
accept / decline actions. Accepted invites move the inviting game into
`active-past`; declined invites disappear from the list.
-->
<script lang="ts">
import { onMount } from "svelte";
import { account } from "$lib/account-store.svelte";
import { i18n } from "$lib/i18n/index.svelte";
import { lobbyData, describeLobbyError } from "$lib/lobby-data.svelte";
import LobbyShell from "./lobby-shell.svelte";
import {
declineInvite,
listMyGames,
redeemInvite,
type InviteSummary,
} from "../../api/lobby";
let actionInFlight = $state<string | null>(null);
let actionError = $state<string | null>(null);
onMount(() => {
lobbyData.ensure().then((client) => {
if (client !== null) {
account.ensure(client).catch(() => {});
}
});
});
async function acceptInvite(invite: InviteSummary): Promise<void> {
const client = lobbyData.client;
if (client === null) return;
actionInFlight = invite.inviteId;
actionError = null;
try {
await redeemInvite(client, invite.gameId, invite.inviteId);
lobbyData.removeInvitation(invite.inviteId);
lobbyData.setMyGames(await listMyGames(client));
} catch (err) {
actionError = describeLobbyError(err);
} finally {
actionInFlight = null;
}
}
async function rejectInvite(invite: InviteSummary): Promise<void> {
const client = lobbyData.client;
if (client === null) return;
actionInFlight = invite.inviteId;
actionError = null;
try {
await declineInvite(client, invite.gameId, invite.inviteId);
lobbyData.removeInvitation(invite.inviteId);
} catch (err) {
actionError = describeLobbyError(err);
} finally {
actionInFlight = null;
}
}
</script>
<LobbyShell>
{#if lobbyData.configError !== null}
<p role="alert" data-testid="account-error">{lobbyData.configError}</p>
{:else if lobbyData.error !== null}
<p role="alert" data-testid="lobby-error">{lobbyData.error}</p>
{/if}
{#if actionError !== null}
<p role="alert" data-testid="lobby-invitation-error">{actionError}</p>
{/if}
<section data-testid="lobby-games-invitations-section">
<h2>{i18n.t("lobby.section.invitations")}</h2>
{#if lobbyData.loading}
<p role="status">{i18n.t("lobby.list_loading")}</p>
{:else if lobbyData.invitations.length === 0}
<p data-testid="lobby-invitations-empty">{i18n.t("lobby.invitations.empty")}</p>
{:else}
<ul class="card-list">
{#each lobbyData.invitations as invite (invite.inviteId)}
<li class="card">
<strong>{invite.raceName}</strong>
<span class="meta">{invite.gameId}</span>
<div class="actions">
<button
onclick={() => acceptInvite(invite)}
disabled={actionInFlight === invite.inviteId}
data-testid="lobby-invite-accept"
>
{i18n.t("lobby.invitation.accept")}
</button>
<button
onclick={() => rejectInvite(invite)}
disabled={actionInFlight === invite.inviteId}
data-testid="lobby-invite-decline"
>
{i18n.t("lobby.invitation.decline")}
</button>
</div>
</li>
{/each}
</ul>
{/if}
</section>
</LobbyShell>
<style>
section { margin-bottom: var(--space-6); }
section h2 { font-size: var(--text-lg); margin: 0 0 var(--space-3); }
.card-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.card {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface-raised);
text-align: left;
font: inherit;
width: 100%;
}
.meta { color: var(--color-text-muted); font-size: var(--text-sm); }
.actions { display: flex; gap: var(--space-2); margin-top: var(--space-1); }
</style>