Stage 8: UI social/account/history surfaces
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode -> backend REST -> existing domain services): friends (incl. one-time friend codes), per-user blocks, friend-game invitations, profile editing + email binding, the statistics screen, and the in-game history + GCG export. Friends gain two add paths (interview decision, a deliberate plan change): one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited redeem); and play-gated requests (shared game required) where an explicit decline is permanent, an ignored request lapses after 30 days, and a code bypasses a decline. Migration 00006 widens friendships_status_chk and adds friend_codes. Lobby notification badge is poll + push: a new generic `notify` event drives it live; the client polls on open/focus. Language stays a single Settings control that writes through to the durable account's preferred_language. GCG export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file. Tests: backend unit + inttest (friend gate/decline/code, ListInvitations, GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN (Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, TESTING, module READMEs.
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
// The header hamburger + dropdown, shared by the lobby and game screens.
|
||||
let { items }: { items: { label: string; onclick: () => void }[] } = $props();
|
||||
// The header hamburger + dropdown, shared by the lobby and game screens. An item
|
||||
// may carry a numeric badge; the hamburger shows the total via the `badge` prop so
|
||||
// a pending count is visible while the menu is closed.
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
onclick: () => void;
|
||||
badge?: number;
|
||||
}
|
||||
let { items, badge = 0 }: { items: MenuItem[]; badge?: number } = $props();
|
||||
let open = $state(false);
|
||||
|
||||
function pick(fn: () => void) {
|
||||
@@ -12,6 +19,7 @@
|
||||
<div class="menu">
|
||||
<button class="burger" onclick={() => (open = !open)} aria-label="Menu">
|
||||
<span></span><span></span><span></span>
|
||||
{#if badge > 0}<span class="dot" data-testid="menu-badge">{badge}</span>{/if}
|
||||
</button>
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
@@ -19,7 +27,10 @@
|
||||
<div class="backdrop" onclick={() => (open = false)}></div>
|
||||
<div class="dropdown">
|
||||
{#each items as it (it.label)}
|
||||
<button onclick={() => pick(it.onclick)}>{it.label}</button>
|
||||
<button onclick={() => pick(it.onclick)}>
|
||||
<span>{it.label}</span>
|
||||
{#if it.badge}<span class="idot">{it.badge}</span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -31,6 +42,7 @@
|
||||
display: inline-flex;
|
||||
}
|
||||
.burger {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 44px;
|
||||
@@ -43,6 +55,33 @@
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.dot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 0;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--danger, #c0392b);
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
.idot {
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--danger, #c0392b);
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
.burger span {
|
||||
display: block;
|
||||
height: 3px;
|
||||
@@ -69,6 +108,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
.dropdown button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
background: none;
|
||||
|
||||
Reference in New Issue
Block a user