import { lazy, Suspense, useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation, useNavigationType } from 'react-router-dom'; import { useGameStore } from './store/gameStore'; import { socket, emit } from './lib/socket'; import { trackEvent } from './lib/track'; import type { MyPlayer } from './types'; import GameList from './pages/GameList'; import Lobby from './pages/Lobby'; import GameTable from './pages/GameTable'; import Auth from './pages/Auth'; import History from './pages/History'; // Admin pulls in recharts — lazy-load it so players never download that chunk. const AdminLayout = lazy(() => import('./pages/admin/AdminLayout')); const AdminStats = lazy(() => import('./pages/admin/AdminStats')); function AppInner() { const navigate = useNavigate(); const location = useLocation(); const navigationType = useNavigationType(); const account = useGameStore((s) => s.account); const myPlayer = useGameStore((s) => s.myPlayer); const gameStatus = useGameStore((s) => s.gameStatus); const error = useGameStore((s) => s.error); const clearError = useGameStore((s) => s.clearError); // Reconnect into an in-progress game on every socket connect (after network drops). useEffect(() => { const doReconnect = () => { const saved = localStorage.getItem('bridzik_player'); if (!saved) return; const player = JSON.parse(saved) as MyPlayer; emit.reconnectToGame(player.gid, player.token); }; socket.on('connect', doReconnect); if (socket.connected) doReconnect(); return () => { socket.off('connect', doReconnect); }; }, []); // Single authoritative navigation: // - not logged in → /auth // - logged in & in a game → lobby/game // - logged in, no game, on /auth (just logged in) → leave for the game list // - logged in, no game, elsewhere → null (let the user navigate: list/history) const onGameRoute = location.pathname === '/auth' || location.pathname.startsWith('/game') || location.pathname.startsWith('/lobby'); // /admin is a separate concern gated by its own token, not the player login. const onAdminRoute = location.pathname.startsWith('/admin'); const targetRoute = onAdminRoute ? null : !account ? '/auth' : myPlayer ? gameStatus ? `/game/${gameStatus.gid}` : `/lobby/${myPlayer.gid}` : onGameRoute ? '/' : null; useEffect(() => { if (!targetRoute) return; navigate(targetRoute, { replace: true }); }, [targetRoute, navigate]); // Auto-dismiss errors after 4 s useEffect(() => { if (!error) return; const t = setTimeout(clearError, 4000); return () => clearTimeout(t); }, [error, clearError]); // Fire-and-forget pageview beacon for self-hosted analytics; must never // affect the app (network errors are swallowed). Skip REPLACE navigations -- // those are app-internal gate/index redirects (login gate, /admin index -> // stats), not a page the user actually navigated to, so they'd otherwise // inflate the count with one extra row per redirect hop. Skip /admin itself // too -- that's the dashboard viewing its own traffic, not player usage. // Skip /, /lobby, /game -- high-frequency in-game navigation with no // analytical value; the backend drops these anyway (api/stats.py _SKIPPED_PATHS). // The visitor's first touch (incl. landings on "/", which both skips would // otherwise swallow) is captured by trackLanding() in main.tsx instead. // Skip /auth too: it's only ever reached by a full page load (every in-app // redirect to it is REPLACE), so each /auth row would be a 1:1 duplicate of // the landing event fired by that same load. useEffect(() => { if (navigationType === 'REPLACE') return; if (location.pathname.startsWith('/admin')) return; if ( location.pathname === '/' || location.pathname === '/auth' || location.pathname.startsWith('/lobby') || location.pathname.startsWith('/game') ) { return; } trackEvent(location.pathname, document.referrer); }, [location.pathname, navigationType]); return ( <> {error && (