frontendove optimalizacie
This commit is contained in:
@@ -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 { BrowserRouter, Routes, Route, Navigate, useNavigate, useLocation, useNavigationType } from 'react-router-dom';
|
||||||
import { useGameStore } from './store/gameStore';
|
import { useGameStore } from './store/gameStore';
|
||||||
import { socket, emit } from './lib/socket';
|
import { socket, emit } from './lib/socket';
|
||||||
@@ -8,8 +8,10 @@ import Lobby from './pages/Lobby';
|
|||||||
import GameTable from './pages/GameTable';
|
import GameTable from './pages/GameTable';
|
||||||
import Auth from './pages/Auth';
|
import Auth from './pages/Auth';
|
||||||
import History from './pages/History';
|
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() {
|
function AppInner() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -89,6 +91,7 @@ function AppInner() {
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Suspense fallback={<p className="text-center text-green-dim pt-20 font-serif italic">Načítava sa…</p>}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/auth" element={<Auth />} />
|
<Route path="/auth" element={<Auth />} />
|
||||||
<Route path="/" element={<GameList />} />
|
<Route path="/" element={<GameList />} />
|
||||||
@@ -101,6 +104,7 @@ function AppInner() {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { emit } from '../lib/socket';
|
import { emit } from '../lib/socket';
|
||||||
|
import { forbiddenGuess } from '../lib/gameRules';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cardsInRound: number;
|
cardsInRound: number;
|
||||||
@@ -9,11 +10,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function GuessControls({ cardsInRound, guesses, myOrder, activePlayer, activePlayerName }: Props) {
|
export default function GuessControls({ cardsInRound, guesses, myOrder, activePlayer, activePlayerName }: Props) {
|
||||||
const guessCount = Object.keys(guesses).length;
|
|
||||||
const isMyTurn = activePlayer === myOrder;
|
const isMyTurn = activePlayer === myOrder;
|
||||||
const isLastToGuess = guessCount === 3;
|
const forbidden = forbiddenGuess(cardsInRound, guesses);
|
||||||
const alreadySum = Object.values(guesses).reduce((a, b) => a + b, 0);
|
|
||||||
const forbidden = isLastToGuess ? cardsInRound - alreadySum : -1;
|
|
||||||
|
|
||||||
if (!isMyTurn) {
|
if (!isMyTurn) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,3 +12,12 @@ export function computePlayable(hand: Hand, ledColor: CardColor | null): Set<str
|
|||||||
|
|
||||||
return new Set(keys);
|
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';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const QUERY = '(min-width: 1024px)';
|
||||||
|
|
||||||
/** True on viewports >= 1024px — drives the desktop GameTable layout
|
/** True on viewports >= 1024px — drives the desktop GameTable layout
|
||||||
* (score sidebar, larger oval, bigger cards). */
|
* (score sidebar, larger oval, bigger cards). */
|
||||||
export function useIsDesktop(): boolean {
|
export function useIsDesktop(): boolean {
|
||||||
const query = '(min-width: 1024px)';
|
|
||||||
const [isDesktop, setIsDesktop] = useState(
|
const [isDesktop, setIsDesktop] = useState(
|
||||||
() => typeof window !== 'undefined' && window.matchMedia(query).matches,
|
() => typeof window !== 'undefined' && window.matchMedia(QUERY).matches,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mql = window.matchMedia(query);
|
const mql = window.matchMedia(QUERY);
|
||||||
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
|
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
|
||||||
mql.addEventListener('change', handler);
|
mql.addEventListener('change', handler);
|
||||||
setIsDesktop(mql.matches);
|
|
||||||
return () => mql.removeEventListener('change', handler);
|
return () => mql.removeEventListener('change', handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export default function GameTable() {
|
|||||||
const lingerTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const lingerTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const previousStash = gameStatus?.status.previous_stash ?? 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(() => {
|
useEffect(() => {
|
||||||
if (!previousStash) return;
|
if (!previousStash) return;
|
||||||
@@ -43,7 +48,7 @@ export default function GameTable() {
|
|||||||
if (lingerTimer.current) clearTimeout(lingerTimer.current);
|
if (lingerTimer.current) clearTimeout(lingerTimer.current);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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) {
|
if (!gameStatus || !myPlayer) {
|
||||||
return <p className="text-center text-green-dim pt-20 font-serif italic">Načítava sa…</p>;
|
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 topP = seat(2);
|
||||||
const rightP = seat(3);
|
const rightP = seat(3);
|
||||||
|
|
||||||
const wonOf = (o?: number) => (o === undefined ? 0 : active_round_stashes?.[o] ?? 0);
|
// Live round state of one seat, shaped as PlayerCircle props.
|
||||||
const guessOf = (o?: number): number | null => {
|
const seatProps = (o?: number) => ({
|
||||||
if (o === undefined) return null;
|
won: o === undefined ? 0 : active_round_stashes?.[o] ?? 0,
|
||||||
const g = active_round_guesses?.[String(o)];
|
guess: (o === undefined ? null : active_round_guesses?.[String(o)] ?? null) as number | null,
|
||||||
return g === undefined ? null : g;
|
active: o !== undefined && active_player === o,
|
||||||
};
|
});
|
||||||
const activeOf = (o?: number) => o !== undefined && active_player === o;
|
|
||||||
|
|
||||||
// Exact cards still in a player's hand: started with cards_in_round, lost one
|
// 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.
|
// 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;
|
const canEnd = myOrder === 0 || !hostConnected;
|
||||||
|
|
||||||
// ── shared pieces ────────────────────────────────────────────────
|
// ── shared pieces ────────────────────────────────────────────────
|
||||||
const bannerText = activeOf(myOrder)
|
const bannerText = active_player === myOrder
|
||||||
? isPlayPhase
|
? isPlayPhase
|
||||||
? 'Zahraj kartu'
|
? 'Zahraj kartu'
|
||||||
: 'Zadaj tip'
|
: 'Zadaj tip'
|
||||||
@@ -171,39 +175,21 @@ export default function GameTable() {
|
|||||||
|
|
||||||
const topSeat = (
|
const topSeat = (
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
<PlayerCircle
|
<PlayerCircle name={topP?.name ?? '—'} {...seatProps(topP?.order)} size={desktop ? 64 : 52} />
|
||||||
name={topP?.name ?? '—'}
|
|
||||||
won={wonOf(topP?.order)}
|
|
||||||
guess={guessOf(topP?.order)}
|
|
||||||
active={activeOf(topP?.order)}
|
|
||||||
size={desktop ? 64 : 52}
|
|
||||||
/>
|
|
||||||
<FaceDownCards count={cardsInHandOf(topP?.order)} direction="row" desktop={desktop} />
|
<FaceDownCards count={cardsInHandOf(topP?.order)} direction="row" desktop={desktop} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sideSeat = (p?: PlayerInfo) => (
|
const sideSeat = (p?: PlayerInfo) => (
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
<PlayerCircle
|
<PlayerCircle name={p?.name ?? '—'} {...seatProps(p?.order)} size={desktop ? 60 : 48} />
|
||||||
name={p?.name ?? '—'}
|
|
||||||
won={wonOf(p?.order)}
|
|
||||||
guess={guessOf(p?.order)}
|
|
||||||
active={activeOf(p?.order)}
|
|
||||||
size={desktop ? 60 : 48}
|
|
||||||
/>
|
|
||||||
<FaceDownCards count={cardsInHandOf(p?.order)} direction="col" desktop={desktop} />
|
<FaceDownCards count={cardsInHandOf(p?.order)} direction="col" desktop={desktop} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const meSeat = (
|
const meSeat = (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<PlayerCircle
|
<PlayerCircle name={myPlayer.name} {...seatProps(myOrder)} size={desktop ? 70 : 58} />
|
||||||
name={myPlayer.name}
|
|
||||||
won={wonOf(myOrder)}
|
|
||||||
guess={guessOf(myOrder)}
|
|
||||||
active={activeOf(myOrder)}
|
|
||||||
size={desktop ? 70 : 58}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ function GameDetailView({ detail, onBack }: { detail: GameDetail; onBack: () =>
|
|||||||
const blocks: number[][] = [];
|
const blocks: number[][] = [];
|
||||||
for (let i = 0; i < seriesNums.length; i += 2) blocks.push(seriesNums.slice(i, i + 2));
|
for (let i = 0; i < seriesNums.length; i += 2) blocks.push(seriesNums.slice(i, i + 2));
|
||||||
return blocks.map((block, bi) => {
|
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);
|
const roundNums = [...new Set([...roundsOf(sA), ...roundsOf(sB)])].sort((a, b) => a - b);
|
||||||
return (
|
return (
|
||||||
<Fragment key={bi}>
|
<Fragment key={bi}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useGameStore } from '../store/gameStore';
|
import { useGameStore } from '../store/gameStore';
|
||||||
import { emit } from '../lib/socket';
|
import { emit } from '../lib/socket';
|
||||||
|
import { leaveGame } from '../lib/leaveGame';
|
||||||
|
|
||||||
export default function Lobby() {
|
export default function Lobby() {
|
||||||
const { gid } = useParams<{ gid: string }>();
|
const { gid } = useParams<{ gid: string }>();
|
||||||
@@ -13,12 +14,7 @@ export default function Lobby() {
|
|||||||
const isHost = myPlayer?.order === 0;
|
const isHost = myPlayer?.order === 0;
|
||||||
const canStart = players.length === 4 && isHost;
|
const canStart = players.length === 4 && isHost;
|
||||||
|
|
||||||
const handleLeave = () => {
|
const handleLeave = () => leaveGame(navigate);
|
||||||
emit.leaveGame();
|
|
||||||
useGameStore.getState().reset();
|
|
||||||
localStorage.removeItem('bridzik_player');
|
|
||||||
navigate('/', { replace: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopyCode = () => {
|
const handleCopyCode = () => {
|
||||||
if (gid) navigator.clipboard.writeText(gid);
|
if (gid) navigator.clipboard.writeText(gid);
|
||||||
|
|||||||
Reference in New Issue
Block a user