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);