Play: app-safe in-app Back + canonicalize shareable URLs (Codex audit)
* In-app Back arrow is now deterministic on deep links: if there's in-app history it pops (history.back); otherwise it navigates to the parent screen (game → selection → hub) instead of leaving the site. Device Back stays native. * Canonicalize ?game/?v: unknown game → hub; an invalid v for the game (e.g. word&v=large or wordsearch&v=5) → the game's default, via replaceState so the URL is clean and local-storage keys/status match. Derived variant/size are also clamped so a bad URL can never crash the game with an invalid variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@
|
|||||||
let sp = $derived($page.url.searchParams);
|
let sp = $derived($page.url.searchParams);
|
||||||
let game = $derived(sp.get('game') === 'wordsearch' ? 'wordsearch' : 'word');
|
let game = $derived(sp.get('game') === 'wordsearch' ? 'wordsearch' : 'word');
|
||||||
let view = $derived(!sp.get('game') ? 'hub' : (sp.get('v') ? 'play' : 'select'));
|
let view = $derived(!sp.get('game') ? 'hub' : (sp.get('v') ? 'play' : 'select'));
|
||||||
let variant = $derived(sp.get('v') || '5');
|
let variant = $derived(['5', '6'].includes(sp.get('v')) ? sp.get('v') : '5');
|
||||||
let wsSize = $derived(sp.get('v') || 'med');
|
let wsSize = $derived(['small', 'med', 'large'].includes(sp.get('v')) ? sp.get('v') : 'med');
|
||||||
|
|
||||||
let date = $state('');
|
let date = $state('');
|
||||||
let wordStatus = $state({ 5: null, 6: null });
|
let wordStatus = $state({ 5: null, 6: null });
|
||||||
@@ -78,11 +78,28 @@
|
|||||||
return 'Play';
|
return 'Play';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward navigations push a history entry (via goto); the in-app Back button
|
// Forward navigations push a history entry (via goto). The in-app Back button
|
||||||
// pops it (history.back), so it mirrors the device Back button exactly.
|
// pops it (history.back) when we have in-app history, but on a direct deep link
|
||||||
function openGame(g) { goto('/play?game=' + g); }
|
// (no in-app history) it navigates to the parent screen instead of leaving the
|
||||||
function pick(v) { goto('/play?game=' + game + '&v=' + v); }
|
// site. Device Back stays browser-native either way.
|
||||||
function back() { history.back(); }
|
let appNavDepth = 0;
|
||||||
|
function openGame(g) { appNavDepth++; goto('/play?game=' + g); }
|
||||||
|
function pick(v) { appNavDepth++; goto('/play?game=' + game + '&v=' + v); }
|
||||||
|
function back() {
|
||||||
|
if (appNavDepth > 0) { appNavDepth--; history.back(); }
|
||||||
|
else goto(view === 'play' ? '/play?game=' + game : '/play');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize shareable/bookmarked URLs: unknown game → hub; invalid v for the
|
||||||
|
// game → its default (replaceState, so it doesn't add a history entry).
|
||||||
|
$effect(() => {
|
||||||
|
const g = sp.get('game'), v = sp.get('v');
|
||||||
|
if (g && g !== 'word' && g !== 'wordsearch') { goto('/play', { replaceState: true }); return; }
|
||||||
|
if (g && v) {
|
||||||
|
const valid = g === 'word' ? ['5', '6'] : ['small', 'med', 'large'];
|
||||||
|
if (!valid.includes(v)) goto(`/play?game=${g}&v=${g === 'word' ? '5' : 'med'}`, { replaceState: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Daily Word on mobile = a focused viewport: lock scroll + hide footer. Cleanup
|
// Daily Word on mobile = a focused viewport: lock scroll + hide footer. Cleanup
|
||||||
// ALWAYS removes the class (re-run or unmount), so leaving /play can't strand it.
|
// ALWAYS removes the class (re-run or unmount), so leaving /play can't strand it.
|
||||||
|
|||||||
Reference in New Issue
Block a user