Stage 8 polish: profile validation, finished-game UI, badge + Safari fixes
Owner-review follow-up on the Stage 8 branch: - Friend code is copyable (📋 + toast). The lobby notification badge is fixed — it had inherited the hamburger-bar style — into a proper round count dot. - Safari: min-width:0 on flex text inputs (friend code, profile, chat) so they shrink instead of pushing the adjacent button off-screen. - Profile editing is validated on both the UI and the backend: display-name format (letters joined by single space/./_ separators, no leading/trailing/adjacent separators, <=32 runes), a UTC-offset timezone picker (account.ResolveZone parses ±HH:MM or a legacy IANA name), a 10-minute away grid capped at 12h (wrap-aware), and email format; Save is disabled and invalid fields red-bordered until valid. Language stays in Settings. - In a game, an "add to friends" menu item flips to a disabled "request sent"; chat send/nudge became ⬆️/🛎️ icon buttons. - A finished game drops its last-word highlight, hides Check word / Drop game, disables zoom, and draws an inert (greyed) footer instead of hiding it. Tests: account validators (name/away/zone), UI profileValidation, e2e for the finished-game footer/menu and the copy control. Docs (PLAN, ARCHITECTURE, FUNCTIONAL +ru, UI_DESIGN) updated for the display-name rule, UTC-offset timezone and the 12h away window.
This commit is contained in:
+36
-22
@@ -66,7 +66,7 @@
|
||||
// Highlight the last word with a dark tile bg; while placing, only the pending tiles
|
||||
// are highlighted. It flashes when the opponent just moved and it is now our turn.
|
||||
const highlight = $derived(
|
||||
placement.pending.length > 0 || !lastPlay
|
||||
placement.pending.length > 0 || !lastPlay || (!!view && view.game.status !== 'active')
|
||||
? new Set<string>()
|
||||
: new Set(lastPlay.tiles.map((tt) => `${tt.row},${tt.col}`)),
|
||||
);
|
||||
@@ -378,9 +378,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
let requested = $state(new Set<string>());
|
||||
const noop = () => {};
|
||||
|
||||
async function addFriend(accountId: string) {
|
||||
try {
|
||||
await gateway.friendRequest(accountId);
|
||||
requested = new Set([...requested, accountId]);
|
||||
showToast(t('friends.requestSent'));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
@@ -391,15 +395,21 @@
|
||||
view ? view.game.seats.filter((s) => s.accountId !== app.session?.userId) : [],
|
||||
);
|
||||
|
||||
// In a finished game the menu drops Check word and Drop game, gains Export GCG, and
|
||||
// an "add to friends" item flips to a disabled "request sent" once tapped.
|
||||
const menuItems = $derived([
|
||||
{ label: t('game.history'), onclick: () => (historyOpen = true) },
|
||||
{ label: t('game.chat'), onclick: openChat },
|
||||
{ label: t('game.checkWord'), onclick: openCheck },
|
||||
...(gameOver ? [] : [{ label: t('game.checkWord'), onclick: openCheck }]),
|
||||
...(view?.game.status === 'finished' ? [{ label: t('game.exportGcg'), onclick: exportGcg }] : []),
|
||||
...(!app.profile?.isGuest
|
||||
? opponents.map((s) => ({ label: `${t('friends.addFromGame')}: ${s.displayName}`, onclick: () => addFriend(s.accountId) }))
|
||||
? opponents.map((s) =>
|
||||
requested.has(s.accountId)
|
||||
? { label: t('game.requestSent'), onclick: noop, disabled: true }
|
||||
: { label: `${t('friends.addFromGame')}: ${s.displayName}`, onclick: () => addFriend(s.accountId) },
|
||||
)
|
||||
: []),
|
||||
{ label: t('game.dropGame'), onclick: () => (resignOpen = true) },
|
||||
...(gameOver ? [] : [{ label: t('game.dropGame'), onclick: () => (resignOpen = true) }]),
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -454,7 +464,7 @@
|
||||
locale={app.locale}
|
||||
{focus}
|
||||
oncell={onCell}
|
||||
ontogglezoom={() => (zoomed = !zoomed)}
|
||||
ontogglezoom={() => { if (!gameOver) zoomed = !zoomed; }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,28 +481,28 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if !gameOver}
|
||||
<div class="rack-row">
|
||||
<div class="rack-wrap">
|
||||
<Rack {slots} {variant} {selected} ondown={onRackDown} />
|
||||
</div>
|
||||
{#if placement.pending.length > 0}
|
||||
<HoldConfirm triggerClass="make" onhold={commit}>
|
||||
{#snippet trigger()}<span class="flag">🏁</span>{/snippet}
|
||||
{#snippet popover(close)}
|
||||
<button class="pop go" onclick={() => { close(); commit(); }}>{t('game.makeMove')} ✅</button>
|
||||
<button class="pop rs" onclick={() => { close(); resetPlacement(); }}>{t('game.reset')} ❌</button>
|
||||
{/snippet}
|
||||
</HoldConfirm>
|
||||
{/if}
|
||||
<!-- The footer is drawn even when the game is over (rack + tab bar), but inert:
|
||||
a finished game shows the final rack greyed out and the controls disabled. -->
|
||||
<div class="rack-row" class:inert={gameOver}>
|
||||
<div class="rack-wrap">
|
||||
<Rack {slots} {variant} {selected} ondown={onRackDown} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if !gameOver && placement.pending.length > 0}
|
||||
<HoldConfirm triggerClass="make" onhold={commit}>
|
||||
{#snippet trigger()}<span class="flag">🏁</span>{/snippet}
|
||||
{#snippet popover(close)}
|
||||
<button class="pop go" onclick={() => { close(); commit(); }}>{t('game.makeMove')} ✅</button>
|
||||
<button class="pop rs" onclick={() => { close(); resetPlacement(); }}>{t('game.reset')} ❌</button>
|
||||
{/snippet}
|
||||
</HoldConfirm>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="loading">{t('common.loading')}</p>
|
||||
{/if}
|
||||
|
||||
{#snippet tabbar()}
|
||||
{#if view && !gameOver}
|
||||
{#if view}
|
||||
<TabBar>
|
||||
<button class="tab" disabled={busy || !isMyTurn || bagEmpty} onclick={openExchange}>
|
||||
<span class="sq">🔄</span><span class="lbl">{t('game.draw')}</span>
|
||||
@@ -508,7 +518,7 @@
|
||||
{/snippet}
|
||||
{#snippet popover(close)}<button class="pop go" onclick={() => { close(); doHint(); }}>{t('game.confirm')} ✅</button>{/snippet}
|
||||
</HoldConfirm>
|
||||
<button class="tab" disabled={busy || placement.pending.length > 0} onclick={shuffle}>
|
||||
<button class="tab" disabled={busy || gameOver || placement.pending.length > 0} onclick={shuffle}>
|
||||
<span class="sq">🔀</span>
|
||||
</button>
|
||||
</TabBar>
|
||||
@@ -697,6 +707,10 @@
|
||||
align-items: stretch;
|
||||
padding: 0 var(--pad) 6px;
|
||||
}
|
||||
.rack-row.inert {
|
||||
pointer-events: none;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.rack-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user