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 game = $derived(sp.get('game') === 'wordsearch' ? 'wordsearch' : 'word');
|
||||
let view = $derived(!sp.get('game') ? 'hub' : (sp.get('v') ? 'play' : 'select'));
|
||||
let variant = $derived(sp.get('v') || '5');
|
||||
let wsSize = $derived(sp.get('v') || 'med');
|
||||
let variant = $derived(['5', '6'].includes(sp.get('v')) ? sp.get('v') : '5');
|
||||
let wsSize = $derived(['small', 'med', 'large'].includes(sp.get('v')) ? sp.get('v') : 'med');
|
||||
|
||||
let date = $state('');
|
||||
let wordStatus = $state({ 5: null, 6: null });
|
||||
@@ -78,11 +78,28 @@
|
||||
return 'Play';
|
||||
}
|
||||
|
||||
// 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.
|
||||
function openGame(g) { goto('/play?game=' + g); }
|
||||
function pick(v) { goto('/play?game=' + game + '&v=' + v); }
|
||||
function back() { history.back(); }
|
||||
// Forward navigations push a history entry (via goto). The in-app Back button
|
||||
// pops it (history.back) when we have in-app history, but on a direct deep link
|
||||
// (no in-app history) it navigates to the parent screen instead of leaving the
|
||||
// site. Device Back stays browser-native either way.
|
||||
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
|
||||
// ALWAYS removes the class (re-run or unmount), so leaving /play can't strand it.
|
||||
|
||||
Reference in New Issue
Block a user