/** * The whole site: sign in with an ATProto account, design a camo, read what * this is. * * The entry point only. It fills in the navigation a screen cannot import, * finds the masthead the layout rendered, catches anything that escapes, and * hands over to the router. */ import { ApiError } from "./api"; import { collectNavLinks } from "./chrome"; import { render } from "./dom"; import { nav } from "./nav"; import { markEntered, onHashChange, start } from "./router"; import { challengeScreen } from "./screens/challenge"; import { errorScreen } from "./screens/error"; import { matchesScreen } from "./screens/matches"; // The screens a screen can send the player to that it cannot import. See nav.ts. nav.start = start; nav.matches = matchesScreen; nav.markShown = markEntered; // Re-entering without a fresh Mode choice (waiting.ts's Back button) gets // the least restrictive pool, Scenario, rather than silently narrowing to // Duel's. nav.challenge = (session) => challengeScreen(session, "scenario"); /** * Until start() renders, the page is the CSS "Loading…" on #app[aria-busy]. An * exception on the way there would leave that on screen for good, so both * channels an escaping error can take get turned into a screen with a way out. * * One shot: if the error screen's own render is what threw, a second pass would * only loop. */ let trapped = false; function trap(channel: string, reason: unknown): void { if (trapped) return; trapped = true; // Names the channel, and says the trap - not the app - put that screen up. // The browser's own logging of the error is untouched. console.error(`trap: an error escaped the app (${channel})`, reason); render( ...errorScreen( // ApiError messages are written to be read; anything else is a stack // trace's worth of noise the player cannot act on. reason instanceof ApiError ? reason.message : "An unexpected error interrupted the page.", () => { window.location.reload(); }, ), ); } window.addEventListener("error", (event) => { // Failed subresource loads fire here too, with the element as the target. A // missing image is no reason to throw away the whole page. if (event.target !== window) return; trap("error", event.error); }); window.addEventListener("unhandledrejection", (event) => { trap("unhandledrejection", event.reason); }); window.addEventListener("hashchange", onHashChange); collectNavLinks(); void start();