frontendove optimalizacie
This commit is contained in:
+19
-15
@@ -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}
|
||||
</div>
|
||||
)}
|
||||
<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 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user