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 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).
72 lines
2.0 KiB
TypeScript
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;
|
|
}
|
|
}
|