mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 00:48:10 +00:00
Add modal URL router (#modal=name&tab=key) (#3924)
## Description Adds a modal URL router so modals can be opened, deep-linked, and bookmarked via the hash. URLs of the form `#modal=<name>&tab=<key>&...` open the named modal and pass remaining keys as args to `onOpen`. The reverse direction also syncs: opening a modal via the UI updates the URL, closing it clears the hash, and switching tabs updates `&tab=`. Builds on the BaseModal refactor from #3923. ### What's new **`ModalRouter.ts`** — small registry + two-way sync helper. - `register(name, { tag, pageId? })` declares a modal as router-managed - `routeFromHash()` parses `#modal=...` and dispatches to `modal.open(args)` - `syncOpened/syncClosed/syncTab` push state back into the URL via `history.replaceState` (no history entries) - A `routingFromUrl` flag prevents URL→modal→URL feedback loops - Unknown modal names silently strip the hash **`BaseModal`** — opt-in URL sync via a `routerName` property. - When set, BaseModal calls into `modalRouter.syncOpened/syncClosed/syncTab` from `open` / `close` / `setActiveTab` - Modals that own their own URL state (lobby modals) just leave `routerName` undefined **`Main.ts`** — registers all routable modals and wires the router. - `handleUrl()`: adds a `modalRouter.routeFromHash()` branch after the path-based lobby join - `onHashUpdate`: when the hash is router-managed, routes via the router instead of tearing down lobby state ### Routable modals 13 inline modals: store, settings, leaderboard, clan, account, help, news, language, single-player, ranked, troubleshooting, territory-patterns, flag-input. Excluded by design: join-lobby, host-lobby (own URL state via `/game/<id>`), matchmaking (no URL state). ### Example uses - Deep link to store flags tab: `/#modal=store&tab=flags` - Settings keybinds tab: `/#modal=settings&tab=keybinds` - Cosmetics.ts now redirects to `#modal=store&tab=packs` when a hard-currency purchase fails for insufficient plutonium (after the alert), so users can top up directly ### URL behavior - `replaceState` everywhere — no history entries added when modals open / close / switch tabs - Browser back/forward still works for the existing path-based game flow - `hashchange` events are router-aware so external hash changes (back button, manual edit) correctly switch between routed modals without tearing down lobby state ## Please complete the following: - [x] I have added screenshots for all UI updates _(no visual changes; smoke-tested in dev)_ - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file _(no new user-visible strings)_ - [ ] I have added relevant tests to the test directory _(no test coverage; manually tested URL load, UI open, tab switch, close, hashchange, insufficient-plutonium redirect)_ - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
@@ -43,6 +43,7 @@ import { initLayout } from "./Layout";
|
||||
import "./LeaderboardModal";
|
||||
import "./Matchmaking";
|
||||
import { MatchmakingModal } from "./Matchmaking";
|
||||
import { modalRouter } from "./ModalRouter";
|
||||
import { initNavigation } from "./Navigation";
|
||||
import "./NewsModal";
|
||||
import "./PatternInput";
|
||||
@@ -268,6 +269,50 @@ class Client {
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
crazyGamesSDK.maybeInit();
|
||||
|
||||
// Register modals with the URL router. Lobby modals (join/host) and
|
||||
// matchmaking are intentionally omitted — they own their own URL state
|
||||
// (path-based) or none at all.
|
||||
modalRouter.register("store", {
|
||||
tag: "store-modal",
|
||||
pageId: "page-item-store",
|
||||
});
|
||||
modalRouter.register("settings", {
|
||||
tag: "user-setting",
|
||||
pageId: "page-settings",
|
||||
});
|
||||
modalRouter.register("leaderboard", {
|
||||
tag: "leaderboard-modal",
|
||||
pageId: "page-leaderboard",
|
||||
});
|
||||
modalRouter.register("clan", { tag: "clan-modal", pageId: "page-clan" });
|
||||
modalRouter.register("account", {
|
||||
tag: "account-modal",
|
||||
pageId: "page-account",
|
||||
});
|
||||
modalRouter.register("help", { tag: "help-modal", pageId: "page-help" });
|
||||
modalRouter.register("news", { tag: "news-modal", pageId: "page-news" });
|
||||
modalRouter.register("language", {
|
||||
tag: "language-modal",
|
||||
pageId: "page-language",
|
||||
});
|
||||
modalRouter.register("single-player", {
|
||||
tag: "single-player-modal",
|
||||
pageId: "page-single-player",
|
||||
});
|
||||
modalRouter.register("ranked", {
|
||||
tag: "ranked-modal",
|
||||
pageId: "page-ranked",
|
||||
});
|
||||
modalRouter.register("troubleshooting", {
|
||||
tag: "troubleshooting-modal",
|
||||
pageId: "page-troubleshooting",
|
||||
});
|
||||
modalRouter.register("territory-patterns", {
|
||||
tag: "territory-patterns-modal",
|
||||
});
|
||||
modalRouter.register("flag-input", { tag: "flag-input-modal" });
|
||||
|
||||
// Prefetch turnstile token so it is available when
|
||||
// the user joins a lobby.
|
||||
this.turnstileTokenPromise = getTurnstileToken();
|
||||
@@ -525,6 +570,13 @@ class Client {
|
||||
}
|
||||
|
||||
const onHashUpdate = () => {
|
||||
// Router-managed hash changes (#modal=...) are handled by the router
|
||||
// syncing in/out; we don't need to tear down the lobby state for them.
|
||||
if (modalRouter.isHashRouted()) {
|
||||
modalRouter.routeFromHash();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the UI to its initial state
|
||||
this.joinModal?.close();
|
||||
|
||||
@@ -725,6 +777,9 @@ class Client {
|
||||
console.log(`joining lobby ${lobbyId}`);
|
||||
return;
|
||||
}
|
||||
if (modalRouter.routeFromHash()) {
|
||||
return;
|
||||
}
|
||||
if (decodedHash.startsWith("#affiliate=")) {
|
||||
const affiliateCode = decodedHash.replace("#affiliate=", "");
|
||||
strip();
|
||||
|
||||
Reference in New Issue
Block a user