From 3615fa2bb1dadc3d7ad5b2607cf1514377b0fcfc Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 3 Jul 2026 01:11:26 +0200 Subject: [PATCH] frontendove optimalizacie --- frontend/src/App.tsx | 34 +++++++++-------- frontend/src/components/GuessControls.tsx | 6 +-- frontend/src/lib/gameRules.ts | 9 +++++ frontend/src/lib/useIsDesktop.ts | 8 ++-- frontend/src/pages/GameTable.tsx | 46 ++++++++--------------- frontend/src/pages/History.tsx | 2 +- frontend/src/pages/Lobby.tsx | 8 +--- 7 files changed, 53 insertions(+), 60 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 265aaa6..ade9735 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +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'; @@ -8,8 +8,10 @@ import Lobby from './pages/Lobby'; import GameTable from './pages/GameTable'; import Auth from './pages/Auth'; import History from './pages/History'; -import AdminLayout from './pages/admin/AdminLayout'; -import AdminStats from './pages/admin/AdminStats'; + +// 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(); @@ -89,18 +91,20 @@ function AppInner() { {error} )} - - } /> - } /> - } /> - } /> - } /> - }> - } /> - } /> - - } /> - + Načítava sa…

}> + + } /> + } /> + } /> + } /> + } /> + }> + } /> + } /> + + } /> + +
); } diff --git a/frontend/src/components/GuessControls.tsx b/frontend/src/components/GuessControls.tsx index bc7ed1e..ff4a607 100644 --- a/frontend/src/components/GuessControls.tsx +++ b/frontend/src/components/GuessControls.tsx @@ -1,4 +1,5 @@ import { emit } from '../lib/socket'; +import { forbiddenGuess } from '../lib/gameRules'; interface Props { cardsInRound: number; @@ -9,11 +10,8 @@ interface Props { } export default function GuessControls({ cardsInRound, guesses, myOrder, activePlayer, activePlayerName }: Props) { - const guessCount = Object.keys(guesses).length; const isMyTurn = activePlayer === myOrder; - const isLastToGuess = guessCount === 3; - const alreadySum = Object.values(guesses).reduce((a, b) => a + b, 0); - const forbidden = isLastToGuess ? cardsInRound - alreadySum : -1; + const forbidden = forbiddenGuess(cardsInRound, guesses); if (!isMyTurn) { return ( diff --git a/frontend/src/lib/gameRules.ts b/frontend/src/lib/gameRules.ts index a4ced8a..522bddf 100644 --- a/frontend/src/lib/gameRules.ts +++ b/frontend/src/lib/gameRules.ts @@ -12,3 +12,12 @@ export function computePlayable(hand: Hand, ledColor: CardColor | null): Set): number | null { + if (Object.keys(guesses).length !== 3) return null; + const alreadySum = Object.values(guesses).reduce((a, b) => a + b, 0); + return cardsInRound - alreadySum; +} diff --git a/frontend/src/lib/useIsDesktop.ts b/frontend/src/lib/useIsDesktop.ts index d67f0bd..c561ed4 100644 --- a/frontend/src/lib/useIsDesktop.ts +++ b/frontend/src/lib/useIsDesktop.ts @@ -1,18 +1,18 @@ import { useEffect, useState } from 'react'; +const QUERY = '(min-width: 1024px)'; + /** True on viewports >= 1024px — drives the desktop GameTable layout * (score sidebar, larger oval, bigger cards). */ export function useIsDesktop(): boolean { - const query = '(min-width: 1024px)'; const [isDesktop, setIsDesktop] = useState( - () => typeof window !== 'undefined' && window.matchMedia(query).matches, + () => typeof window !== 'undefined' && window.matchMedia(QUERY).matches, ); useEffect(() => { - const mql = window.matchMedia(query); + const mql = window.matchMedia(QUERY); const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches); mql.addEventListener('change', handler); - setIsDesktop(mql.matches); return () => mql.removeEventListener('change', handler); }, []); diff --git a/frontend/src/pages/GameTable.tsx b/frontend/src/pages/GameTable.tsx index 8845c6b..b180e01 100644 --- a/frontend/src/pages/GameTable.tsx +++ b/frontend/src/pages/GameTable.tsx @@ -33,6 +33,11 @@ export default function GameTable() { const lingerTimer = useRef | null>(null); const previousStash = gameStatus?.status.previous_stash ?? null; + // Every game_status payload recreates the stash object, so identify the trick + // by content — the timer must restart only when a *different* trick completes. + const previousStashKey = previousStash + ? `${previousStash.first_player}:${JSON.stringify(previousStash.cards)}` + : null; useEffect(() => { if (!previousStash) return; @@ -43,7 +48,7 @@ export default function GameTable() { if (lingerTimer.current) clearTimeout(lingerTimer.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [gameStatus?.status.previous_stash?.first_player, JSON.stringify(gameStatus?.status.previous_stash?.cards)]); + }, [previousStashKey]); if (!gameStatus || !myPlayer) { return

Načítava sa…

; @@ -84,13 +89,12 @@ export default function GameTable() { const topP = seat(2); const rightP = seat(3); - const wonOf = (o?: number) => (o === undefined ? 0 : active_round_stashes?.[o] ?? 0); - const guessOf = (o?: number): number | null => { - if (o === undefined) return null; - const g = active_round_guesses?.[String(o)]; - return g === undefined ? null : g; - }; - const activeOf = (o?: number) => o !== undefined && active_player === o; + // Live round state of one seat, shaped as PlayerCircle props. + const seatProps = (o?: number) => ({ + won: o === undefined ? 0 : active_round_stashes?.[o] ?? 0, + guess: (o === undefined ? null : active_round_guesses?.[String(o)] ?? null) as number | null, + active: o !== undefined && active_player === o, + }); // Exact cards still in a player's hand: started with cards_in_round, lost one // per completed trick, minus one more if they've already played this trick. @@ -114,7 +118,7 @@ export default function GameTable() { const canEnd = myOrder === 0 || !hostConnected; // ── shared pieces ──────────────────────────────────────────────── - const bannerText = activeOf(myOrder) + const bannerText = active_player === myOrder ? isPlayPhase ? 'Zahraj kartu' : 'Zadaj tip' @@ -171,39 +175,21 @@ export default function GameTable() { const topSeat = (
- +
); const sideSeat = (p?: PlayerInfo) => (
- +
); const meSeat = (
- +
); diff --git a/frontend/src/pages/History.tsx b/frontend/src/pages/History.tsx index c7a71a2..f50b561 100644 --- a/frontend/src/pages/History.tsx +++ b/frontend/src/pages/History.tsx @@ -243,7 +243,7 @@ function GameDetailView({ detail, onBack }: { detail: GameDetail; onBack: () => const blocks: number[][] = []; for (let i = 0; i < seriesNums.length; i += 2) blocks.push(seriesNums.slice(i, i + 2)); return blocks.map((block, bi) => { - const [sA, sB] = [block[0], block[1]]; + const [sA, sB] = block; const roundNums = [...new Set([...roundsOf(sA), ...roundsOf(sB)])].sort((a, b) => a - b); return ( diff --git a/frontend/src/pages/Lobby.tsx b/frontend/src/pages/Lobby.tsx index 16d5e20..5f74bea 100644 --- a/frontend/src/pages/Lobby.tsx +++ b/frontend/src/pages/Lobby.tsx @@ -1,6 +1,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useGameStore } from '../store/gameStore'; import { emit } from '../lib/socket'; +import { leaveGame } from '../lib/leaveGame'; export default function Lobby() { const { gid } = useParams<{ gid: string }>(); @@ -13,12 +14,7 @@ export default function Lobby() { const isHost = myPlayer?.order === 0; const canStart = players.length === 4 && isHost; - const handleLeave = () => { - emit.leaveGame(); - useGameStore.getState().reset(); - localStorage.removeItem('bridzik_player'); - navigate('/', { replace: true }); - }; + const handleLeave = () => leaveGame(navigate); const handleCopyCode = () => { if (gid) navigator.clipboard.writeText(gid);