import { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import './styles/fonts.css'; import './styles/tokens.css'; import './styles/app.css'; import './styles/landing.css'; import { ThemeProvider } from './theme/ThemeProvider'; import { I18nProvider } from './i18n/I18nProvider'; import { Landing } from './Landing'; import { Workspace } from './App'; import { Admin } from './Admin'; import { ProfilePage } from './ProfilePage'; import { PhotosPage } from './PhotosPage'; import { Library } from './Library'; import { go } from './nav'; import { ErrorBoundary } from './ui/ErrorBoundary'; import { OtherWindowNotice, TakenBar, useSingleWindow } from './ui/SingleTab'; import { InstallPrompt } from './ui/InstallPrompt'; import { OfflineBar } from './ui/OfflineBar'; import { installTracking } from './track'; // Six routes, no router: the landing page, the workspace, the strip moderation // screen, the member's own profile, the member's photo folder and the local // catalogue of folders on the visitor's own disk. nginx serves index.html for // all of them (SPA fallback), so this is a pathname check. // // The pages that are not the two heavy screens: they read no catalogue and boot // no renderer, so they are drawn straight from the route and nothing is kept. function page(route: string) { return route.startsWith('/admin') ? ( ) : route.startsWith('/photos') ? ( ) : route.startsWith('/profile') ? ( ) : ( ); } // The studio and the library are the two screens worth a boot: ~20MB of CanvasKit // under one and the whole catalogue under the other. Both used to be mounted for // every visit, which is the 20MB and the read on a page that was shown neither, // and it is what a machine short of memory took for a crash. A route mounts its // screen the first time it is asked for and stays mounted after that, so coming // back from the studio is a screen opening rather than a page loading. const heavy = (route: string) => (route.startsWith('/app') ? 'app' : route.startsWith('/library') ? 'library' : 'other'); function Shell() { const [route, setRoute] = useState(() => window.location.pathname + window.location.search); useEffect(() => { const onClick = (e: MouseEvent) => { if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; const target = e.target as Element | null; const link = target?.closest?.('a'); if (!(link instanceof HTMLAnchorElement)) return; if (link.target || link.hasAttribute('download')) return; const url = new URL(link.href, window.location.href); const here = window.location.pathname + window.location.search; if (url.origin !== window.location.origin || url.hash) return; if (url.pathname + url.search === here) return; e.preventDefault(); go(url.pathname + url.search); }; const onPop = () => setRoute(window.location.pathname + window.location.search); document.addEventListener('click', onClick); window.addEventListener('popstate', onPop); return () => { document.removeEventListener('click', onClick); window.removeEventListener('popstate', onPop); }; }, []); const kind = heavy(route); const [seen, setSeen] = useState(() => new Set([kind])); useEffect(() => { setSeen((prev) => (prev.has(kind) ? prev : new Set(prev).add(kind))); }, [kind]); // One window at a time, decided before either heavy screen is mounted — see // `useSingleWindow`. `null` is the question still being out: nothing is drawn // for it, because drawing the app and then taking it away is worse than the // fraction of a second the answer takes. const { held, taken, take } = useSingleWindow(kind !== 'other'); const blocked = kind !== 'other' && held !== false; return ( <> {blocked ? ( held ? ( ) : null ) : ( <> {seen.has('app') ? (
) : null} {seen.has('library') ? (
) : null} {kind === 'other' ? page(route) : null} )} {taken ? : null} ); } const root = document.getElementById('root'); if (!root) throw new Error('#root missing'); // The traffic counter records this page and then any click on a `data-key` // control, for the life of the page. installTracking(); // No StrictMode: it double-invokes effects, which would load CanvasKit twice and // run the render pipeline twice on every mount. The install offer hangs off the // i18n provider because its copy comes from the dictionaries. The boundary is // the whole app's: an error on the way to the screen used to be a white page. createRoot(root).render( ); // Service worker, production only: in dev it would answer the browser with the // last build's shell instead of vite's modules. Registered after `load` so its // precache does not race the studio's own 20MB of wasm for the connection, and a // browser that refuses it (private mode, a plain-http origin) just loses the // offline shell — nothing else depends on it. if (import.meta.env.PROD && 'serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => {}); }); }