Files
scrabble-game/ui/src/lib/router.svelte.ts
T
Ilia Denisov 70110effd9
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 + word-check as their own screens; in-game unread badge (review item 7)
- 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).
2026-06-08 23:23:05 +02:00

72 lines
2.0 KiB
TypeScript

// Minimal dependency-free hash router. Hash routing survives a reload and works on
// a file:// origin (Capacitor native packaging), where there is no server to honour
// deep paths. The route is a reactive rune so screens re-render on navigation.
export type RouteName =
| 'login'
| 'lobby'
| 'new'
| 'game'
| 'gameChat'
| 'gameCheck'
| 'profile'
| 'settings'
| 'about'
| 'friends'
| 'stats'
| 'notfound';
export interface Route {
name: RouteName;
params: Record<string, string>;
}
function parse(hash: string): Route {
const path = (hash.replace(/^#/, '') || '/').split('?')[0];
const seg = path.split('/').filter(Boolean);
if (seg.length === 0) return { name: 'lobby', params: {} };
switch (seg[0]) {
case 'login':
return { name: 'login', params: {} };
case 'new':
return { name: 'new', params: {} };
case 'game':
if (!seg[1]) return { name: 'notfound', params: {} };
if (seg[2] === 'chat') return { name: 'gameChat', params: { id: seg[1] } };
if (seg[2] === 'check') return { name: 'gameCheck', params: { id: seg[1] } };
return { name: 'game', params: { id: seg[1] } };
case 'profile':
return { name: 'profile', params: {} };
case 'settings':
return { name: 'settings', params: {} };
case 'about':
return { name: 'about', params: {} };
case 'friends':
return { name: 'friends', params: {} };
case 'stats':
return { name: 'stats', params: {} };
default:
return { name: 'notfound', params: {} };
}
}
export const router = $state<{ route: Route }>({
route: parse(typeof location !== 'undefined' ? location.hash : ''),
});
if (typeof window !== 'undefined') {
window.addEventListener('hashchange', () => {
router.route = parse(location.hash);
});
}
/** navigate switches the hash route (and forces a re-parse if it is unchanged). */
export function navigate(path: string): void {
const target = '#' + path;
if (location.hash === target) {
router.route = parse(target);
} else {
location.hash = path;
}
}