word-waltz / commit
suggested fixes
src/components/Keyboard.ts | 6 ++++-- src/components/ShareDialog.ts | 19 +++++++++++++++++-- src/components/WordleBoard.ts | 14 +++++++++----- src/main.ts | 20 ++++++++++++++++---- src/styles/main.css | 2 +- src/utils/shareLink.ts | 1 + 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/components/Keyboard.ts b/src/components/Keyboard.ts index f680006..8252799 100644 --- a/src/components/Keyboard.ts +++ b/src/components/Keyboard.ts @@ -27,11 +27,11 @@ export function mountKeyboard(container: HTMLElement, props: KeyboardProps): () if (rowStr.startsWith('ENTER')) { // Bottom row: Enter — middle letters — Backspace - appendKey(rowEl, 'Enter', 'Enter', onKey, true); + appendKey(rowEl, 'Enter', 'Enter', onKey, true, 'Submit guess'); for (const ch of rowStr.slice('ENTER'.length, -1)) { appendKey(rowEl, ch, ch, onKey, false); } - appendKey(rowEl, '⌫', 'Backspace', onKey, true); + appendKey(rowEl, '⌫', 'Backspace', onKey, true, 'Delete letter'); } else { for (const ch of rowStr) { appendKey(rowEl, ch, ch, onKey, false); @@ -64,12 +64,14 @@ function appendKey( key: string, onKey: (k: string) => void, wide: boolean, + ariaLabel?: string, ): void { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'keyboard__key' + (wide ? ' keyboard__key--wide' : ''); btn.textContent = label; btn.dataset['key'] = key; + if (ariaLabel) btn.setAttribute('aria-label', ariaLabel); btn.addEventListener('click', () => onKey(key)); row.appendChild(btn); } diff --git a/src/components/ShareDialog.ts b/src/components/ShareDialog.ts index 2940aa7..700e888 100644 --- a/src/components/ShareDialog.ts +++ b/src/components/ShareDialog.ts @@ -29,11 +29,21 @@ export function mountShareDialog(container: HTMLElement, props: ShareDialogProps copyBtn.className = 'share-dialog__copy'; copyBtn.type = 'button'; copyBtn.textContent = 'Copy link'; + let resetTimer: ReturnType<typeof setTimeout> | null = null; copyBtn.addEventListener('click', () => { + if (copyBtn.disabled) return; + copyBtn.disabled = true; navigator.clipboard.writeText(shareUrl).then(() => { copyBtn.textContent = 'Copied!'; - setTimeout(() => { copyBtn.textContent = 'Copy link'; }, 2000); - }).catch(() => { copyBtn.textContent = 'Copy failed'; }); + }).catch(() => { + copyBtn.textContent = 'Copy failed'; + }).finally(() => { + if (resetTimer !== null) clearTimeout(resetTimer); + resetTimer = setTimeout(() => { + copyBtn.textContent = 'Copy link'; + copyBtn.disabled = false; + }, 2000); + }); }); root.appendChild(canvas); @@ -43,6 +53,11 @@ export function mountShareDialog(container: HTMLElement, props: ShareDialogProps QRCode.toCanvas(canvas, shareUrl, { width: 220, margin: 2 }).catch((err: unknown) => { console.error('QR generation failed:', err); + canvas.style.display = 'none'; + const qrError = document.createElement('p'); + qrError.className = 'share-dialog__qr-error'; + qrError.textContent = 'QR code unavailable — copy the link below.'; + root.insertBefore(qrError, linkEl); }); return function cleanup() { diff --git a/src/components/WordleBoard.ts b/src/components/WordleBoard.ts index 3a468b9..5708506 100644 --- a/src/components/WordleBoard.ts +++ b/src/components/WordleBoard.ts @@ -179,18 +179,22 @@ export function mountWordleBoard(container: HTMLElement, props: WordleBoardProps } let endTimer: ReturnType<typeof setTimeout> | null = null; + let destroyed = false; function scheduleRoundEnd(solved: boolean): void { endTimer = setTimeout(() => { - onRoundEnd({ - solved, - guesses, - score: calculateScore(guesses.length, solved), - }); + if (!destroyed) { + onRoundEnd({ + solved, + guesses, + score: calculateScore(guesses.length, solved), + }); + } }, 1600); } return function cleanup() { + destroyed = true; if (endTimer !== null) clearTimeout(endTimer); cleanupKeyboard(); container.removeChild(root); diff --git a/src/main.ts b/src/main.ts index a7f6609..88d1055 100644 --- a/src/main.ts +++ b/src/main.ts @@ -158,6 +158,13 @@ function render( if (solver === myRole) { const round = state.rounds[state.currentRound]; + if (!round) { + // currentRound is temporarily out of bounds (e.g. between write + // and the real-time subscription delivering the new round). + mainEl.innerHTML = '<p class="status">Loading…</p>'; + cleanupView = () => { cleanupScoreboard(); }; + return; + } cleanupMain = mountWordleBoard(mainEl, { secretWord: round.word, onRoundEnd: async (result) => { @@ -241,7 +248,7 @@ function showNewGameScreen(app: HTMLElement): void { const envelope = await encrypt(JSON.stringify(initialState), cryptoKey); - // Race the Firestore write against a 10 s timeout so we surface + // Race the Firestore write against an 8 s timeout so we surface // errors (e.g. database not created, network issue) instead of // hanging on "Creating…" forever. const timeout = new Promise<never>((_, reject) => @@ -298,7 +305,11 @@ function promptPlayerBName( // --------------------------------------------------------------------------- function showError(app: HTMLElement, message: string): void { - app.innerHTML = `<p class="error">${message}</p>`; + app.innerHTML = ''; + const p = document.createElement('p'); + p.className = 'error'; + p.textContent = message; + app.appendChild(p); } // --------------------------------------------------------------------------- @@ -386,8 +397,9 @@ async function bootstrap(): Promise<void> { } wasMyTurn = myTurnNow; render(app, newState, role, gameId, cryptoKey, shareUrl); - } catch { - // Silently ignore — could be a stale snapshot during writes + } catch (err) { + console.error('Failed to decrypt game update:', err); + showError(app, 'Could not decrypt the latest game update — the link may be corrupted.'); } }); } diff --git a/src/styles/main.css b/src/styles/main.css index aea535e..f9e5a66 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -399,7 +399,7 @@ header { } .word-picker__input { - width: 160px; + width: min(100%, 160px); font-size: 1.4rem; font-family: var(--font-mono); font-weight: 700; diff --git a/src/utils/shareLink.ts b/src/utils/shareLink.ts index cd303ef..f45447a 100644 --- a/src/utils/shareLink.ts +++ b/src/utils/shareLink.ts @@ -11,6 +11,7 @@ // Production URL used when generating share links from localhost so that QR // codes are scannable from devices that can't reach localhost. +// If you deploy to a different host, update this constant. const PRODUCTION_URL = 'https://pavelsafronov.gitlab.io/word-waltz/'; function shareBase(): string {