frontendove optimalizacie

This commit is contained in:
tim
2026-07-03 01:11:26 +02:00
parent 7a5202195b
commit 3615fa2bb1
7 changed files with 53 additions and 60 deletions
+7 -3
View File
@@ -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,6 +91,7 @@ function AppInner() {
{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 />} />
@@ -101,6 +104,7 @@ function AppInner() {
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</>
);
}
+2 -4
View File
@@ -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 (
+9
View File
@@ -12,3 +12,12 @@ export function computePlayable(hand: Hand, ledColor: CardColor | null): Set<str
return new Set(keys);
}
/** The bid the last guesser may not make: the four bids must not sum to the
* number of tricks in the round (mirrors Round.add_player_guess in the engine).
* Returns null while earlier players are still guessing. */
export function forbiddenGuess(cardsInRound: number, guesses: Record<string, number>): number | null {
if (Object.keys(guesses).length !== 3) return null;
const alreadySum = Object.values(guesses).reduce((a, b) => a + b, 0);
return cardsInRound - alreadySum;
}
+4 -4
View File
@@ -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);
}, []);
+16 -30
View File
@@ -33,6 +33,11 @@ export default function GameTable() {
const lingerTimer = useRef<ReturnType<typeof setTimeout> | 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 <p className="text-center text-green-dim pt-20 font-serif italic">Načítava sa</p>;
@@ -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 = (
<div className="flex flex-col items-center gap-1.5">
<PlayerCircle
name={topP?.name ?? '—'}
won={wonOf(topP?.order)}
guess={guessOf(topP?.order)}
active={activeOf(topP?.order)}
size={desktop ? 64 : 52}
/>
<PlayerCircle name={topP?.name ?? '—'} {...seatProps(topP?.order)} size={desktop ? 64 : 52} />
<FaceDownCards count={cardsInHandOf(topP?.order)} direction="row" desktop={desktop} />
</div>
);
const sideSeat = (p?: PlayerInfo) => (
<div className="flex flex-col items-center gap-1.5">
<PlayerCircle
name={p?.name ?? '—'}
won={wonOf(p?.order)}
guess={guessOf(p?.order)}
active={activeOf(p?.order)}
size={desktop ? 60 : 48}
/>
<PlayerCircle name={p?.name ?? '—'} {...seatProps(p?.order)} size={desktop ? 60 : 48} />
<FaceDownCards count={cardsInHandOf(p?.order)} direction="col" desktop={desktop} />
</div>
);
const meSeat = (
<div className="flex justify-center">
<PlayerCircle
name={myPlayer.name}
won={wonOf(myOrder)}
guess={guessOf(myOrder)}
active={activeOf(myOrder)}
size={desktop ? 70 : 58}
/>
<PlayerCircle name={myPlayer.name} {...seatProps(myOrder)} size={desktop ? 70 : 58} />
</div>
);
+1 -1
View File
@@ -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 (
<Fragment key={bi}>
+2 -6
View File
@@ -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);