Chat + word-check as their own screens; in-game unread badge (review item 7)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
- Chat and word-check are now routed screens (/game/:id/chat, /game/:id/check) with a header back to the game and no tab-bar, replacing their modals. The soft keyboard just resizes the visible viewport (tracked into --vvh, which the Screen height uses since iOS does not shrink dvh for the keyboard) with the input pinned to the bottom: no modal relayout, no page jump. Supersedes the earlier bottom-sheet Modal attempt. - A new chat message raises an unread badge on the in-game hamburger + the Chat menu row (per game, cleared on opening the chat), mirroring the lobby badge. - TG native back + the header back chevron return chat/check to their game. - Exposes --tg-safe-top (device notch) for the finalised TG-fullscreen header. Tests: e2e for chat/check opening as their own screens + back. Docs: PLAN, FUNCTIONAL(+ru).
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Screen from '../components/Screen.svelte';
|
||||
import { gateway } from '../lib/gateway';
|
||||
import { handleError, showToast } from '../lib/app.svelte';
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
import { alphabetLetters } from '../lib/alphabet';
|
||||
import { canCheckWord, sanitizeCheckWord } from '../lib/checkword';
|
||||
import type { Variant } from '../lib/model';
|
||||
|
||||
// Word-check on its own screen (Stage 17): unlimited dictionary lookups, each with a
|
||||
// complaint, off the board so the soft keyboard never relayouts the play area.
|
||||
let { id }: { id: string } = $props();
|
||||
|
||||
let variant = $state<Variant>('english');
|
||||
let word = $state('');
|
||||
let result = $state<{ word: string; legal: boolean } | null>(null);
|
||||
let cooling = $state(false);
|
||||
const checked = new Map<string, boolean>();
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
// Include the alphabet so input sanitising + the check accept the variant's letters.
|
||||
const st = await gateway.gameState(id, true);
|
||||
variant = st.game.variant;
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
});
|
||||
|
||||
function onInput(e: Event) {
|
||||
word = sanitizeCheckWord((e.target as HTMLInputElement).value, alphabetLetters(variant));
|
||||
}
|
||||
// Disabled while cooling, for an already-checked word, or an out-of-range length.
|
||||
function canCheck(): boolean {
|
||||
return canCheckWord(word, checked.has(word.trim().toUpperCase()), cooling);
|
||||
}
|
||||
async function runCheck() {
|
||||
if (!canCheck()) return;
|
||||
const w = word.trim().toUpperCase();
|
||||
cooling = true;
|
||||
setTimeout(() => (cooling = false), 5000);
|
||||
try {
|
||||
const r = await gateway.checkWord(id, w, variant);
|
||||
checked.set(w, r.legal);
|
||||
result = { word: w, legal: r.legal };
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
async function complain() {
|
||||
if (!result) return;
|
||||
try {
|
||||
await gateway.complaint(id, result.word, '');
|
||||
showToast(t('game.complaintSent'));
|
||||
result = null;
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Screen title={t('game.checkWord')} back={`/game/${id}`}>
|
||||
<div class="wrap">
|
||||
<div class="check">
|
||||
<input
|
||||
value={word}
|
||||
oninput={onInput}
|
||||
onkeydown={(e) => e.key === 'Enter' && runCheck()}
|
||||
placeholder={t('game.checkWordPrompt')}
|
||||
/>
|
||||
<button onclick={runCheck} disabled={!canCheck()}>{t('game.check')}</button>
|
||||
</div>
|
||||
{#if result}
|
||||
<p class="verdict" class:ok={result.legal} class:bad={!result.legal}>
|
||||
{result.legal
|
||||
? t('game.wordLegal', { word: result.word })
|
||||
: t('game.wordIllegal', { word: result.word })}
|
||||
</p>
|
||||
<button class="complain" onclick={complain}>{t('game.complain')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
</Screen>
|
||||
|
||||
<style>
|
||||
.wrap {
|
||||
padding: 16px var(--pad);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.check {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.check input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.check button {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.check button:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.verdict {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.verdict.ok {
|
||||
color: var(--ok, #2e7d32);
|
||||
}
|
||||
.verdict.bad {
|
||||
color: var(--danger, #c0392b);
|
||||
}
|
||||
.complain {
|
||||
align-self: flex-start;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user