Pageview beacony uz neposielaju player_id z klienta (nedovereny vstup na neautentifikovanom endpointe) -- prihlasenie sa eviduje server-side ako event "login" (aj z registracie), s player_id skutocneho uctu. Admin dashboard dostal prepinac scope vsetci/prihlaseni, ktory konzistentne pocita grafy aj tabulky (kazdy login samostatne, navstevnicky den pre anonymnu navstevnost). PageView.country teraz uklada cely anglicky nazov krajiny namiesto ISO kodu (potrebna zmena schemy). Pridane sledovanie klikov na "Pravidla hry" aj z GameList. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
4.7 KiB
TypeScript
124 lines
4.7 KiB
TypeScript
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).
|
|
useEffect(() => {
|
|
if (navigationType === 'REPLACE') return;
|
|
if (location.pathname.startsWith('/admin')) return;
|
|
if (
|
|
location.pathname === '/' ||
|
|
location.pathname.startsWith('/lobby') ||
|
|
location.pathname.startsWith('/game')
|
|
) {
|
|
return;
|
|
}
|
|
trackEvent(location.pathname, document.referrer);
|
|
}, [location.pathname, navigationType]);
|
|
|
|
return (
|
|
<>
|
|
{error && (
|
|
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg text-sm max-w-xs text-center">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<Suspense fallback={<p className="text-center text-green-dim pt-20 font-serif italic">Načítava sa…</p>}>
|
|
<Routes>
|
|
<Route path="/auth" element={<Auth />} />
|
|
<Route path="/" element={<GameList />} />
|
|
<Route path="/history" element={<History />} />
|
|
<Route path="/lobby/:gid" element={<Lobby />} />
|
|
<Route path="/game/:gid" element={<GameTable />} />
|
|
<Route path="/admin" element={<AdminLayout />}>
|
|
<Route index element={<Navigate to="stats" replace />} />
|
|
<Route path="stats" element={<AdminStats />} />
|
|
</Route>
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<BrowserRouter>
|
|
<AppInner />
|
|
</BrowserRouter>
|
|
);
|
|
}
|