import { createRoot } from 'react-dom/client'; // Import polyfills first import './lib/polyfills.ts'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import App from './App.tsx'; import './index.css'; // Hide Capacitor splash screen once JS is ready // Uses the global Capacitor object to avoid a hard dependency on the package if (typeof window !== 'undefined') { const cap = (window as unknown as { Capacitor?: { Plugins?: { SplashScreen?: { hide: () => Promise } } } }).Capacitor; if (cap?.Plugins?.SplashScreen) { cap.Plugins.SplashScreen.hide().catch(() => {}); } } // Clean share links: a bare /verify/, /card/, /coach// // or /lab/// hits the Netlify SPA fallback (index.html), but // HashRouter needs the route in the hash — rewrite it. if (typeof window !== 'undefined' && !window.location.hash) { const m = window.location.pathname.match(/^\/(verify|card|coach)\/(.+)$/); const labProfile = window.location.pathname.match(/^\/lab\/[^/]+\/[^/]+\/[^/]+$/); if (m) window.location.replace(`/#/${m[1]}/${m[2]}${window.location.search}`); else if (labProfile) window.location.replace(`/#${window.location.pathname}${window.location.search}`); } // Outfit font for body text import '@fontsource-variable/outfit'; // Bebas Neue — bold condensed display font for headlines import '@fontsource/bebas-neue'; // Barlow Condensed — tight condensed font for subheadings and UI labels import '@fontsource/barlow-condensed/400.css'; import '@fontsource/barlow-condensed/600.css'; import '@fontsource/barlow-condensed/700.css'; // Leaflet CSS is loaded via CDN in index.html /** * AMBER CALLBACK PRE-CHECK + REDIRECT * * Amber opens the callbackUrl in a browser tab. Because we use HashRouter, * the callbackUrl has no # fragment — it's a plain path: * https://skate-spots.org/amber-callback?event= * * This IIFE runs before React mounts and: * 1. Captures ?event= from the URL into sessionStorage * 2. If we're on /amber-callback (non-hash path), redirects to /#/amber-callback * so HashRouter can match the route and render AmberCallbackPage */ (function captureAmberCallback() { // Try normal query string first (Amber callback: /amber-callback?event=XXX) let eventParam = new URLSearchParams(window.location.search).get('event'); // Try hash fragment (Capacitor deep link: #/amber-callback?event=XXX) if (!eventParam) { const hash = window.location.hash; const qIdx = hash.indexOf('?'); if (qIdx !== -1) { eventParam = new URLSearchParams(hash.slice(qIdx)).get('event'); } } if (eventParam) { console.log('[Amber Pre-Check] Detected Amber callback, event:', eventParam.substring(0, 16) + '...'); try { sessionStorage.setItem('amber_callback_event', eventParam); sessionStorage.setItem('amber_callback_url', window.location.href); sessionStorage.setItem('amber_callback_timestamp', Date.now().toString()); } catch (e) { console.error('[Amber Pre-Check] Failed to store callback:', e); } // If we landed on /amber-callback (non-hash path), redirect into HashRouter // so React Router can match the route and render AmberCallbackPage. // The event is already saved in sessionStorage above so it survives the redirect. const isHashRoute = window.location.hash.startsWith('#/'); if (!isHashRoute && window.location.pathname.includes('amber-callback')) { console.log('[Amber Pre-Check] Redirecting to HashRouter route: /#/amber-callback'); window.location.replace(window.location.origin + '/#/amber-callback'); // Stop — the page will reload at the correct hash route return; } } })(); /** * STALE-DEPLOY SELF-HEAL * * Deploys replace hashed chunk filenames and delete the old ones. A tab that * was already open still holds the previous manifest, so navigating to a lazy * route loads a chunk that no longer exists (404) and the error boundary shows * "THIS PAGE SLAMMED". Vite emits `vite:preloadError` for exactly this case. * * Reload once to pick up the new assets. The timestamp guard prevents a reload * loop if a build is genuinely broken (the error boundary will show instead). */ (function recoverFromStaleChunks() { const KEY = 'skatespots:chunk-reload-at'; const reload = () => { try { const last = Number(sessionStorage.getItem(KEY) || 0); if (Date.now() - last < 15000) return; // already reloaded recently — don't loop sessionStorage.setItem(KEY, String(Date.now())); } catch { /* sessionStorage unavailable — fall through and reload anyway */ } window.location.reload(); }; window.addEventListener('vite:preloadError', reload); window.addEventListener('unhandledrejection', (e) => { const msg = String((e as PromiseRejectionEvent).reason?.message ?? ''); if (/dynamically imported module|Importing a module script failed|Loading chunk/i.test(msg)) { reload(); } }); })(); createRoot(document.getElementById("root")!).render( ); // Tell the boot crash guard (in index.html) that the app mounted successfully, // so its "did not render" timeout doesn't fire. try { (window as unknown as { __SKATESPOTS_BOOTED__?: () => void }).__SKATESPOTS_BOOTED__?.(); } catch { /* no-op */ }