import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useGameStore } from '../store/gameStore'; import { emit } from '../lib/socket'; import { leaveGame } from '../lib/leaveGame'; import { computePlayable } from '../lib/gameRules'; import { computeTotal } from '../lib/standings'; import { useIsDesktop } from '../lib/useIsDesktop'; import { useFitScale } from '../lib/useFitScale'; import Hand from '../components/Hand'; import GuessControls from '../components/GuessControls'; import Trick from '../components/Trick'; import Standings from '../components/Standings'; import PlayerCircle from '../components/PlayerCircle'; import FaceDownCards from '../components/FaceDownCards'; import GameOver from './GameOver'; import type { PlayerInfo, StashData } from '../types'; const TRICK_LINGER_MS = 3000; export default function GameTable() { const navigate = useNavigate(); const desktop = useIsDesktop(); // Zooms the whole desktop board to fill the window (full width + height), so // cards, circles, text and the header all scale together. Up to 2.6×. const { containerRef, scale, contentWidth } = useFitScale([desktop], 860, 2.6); const myPlayer = useGameStore((s) => s.myPlayer); const gameStatus = useGameStore((s) => s.gameStatus); const hand = useGameStore((s) => s.hand); // Hold the last completed trick visible for TRICK_LINGER_MS after it finishes. const [lingeredStash, setLingeredStash] = useState(null); const lingerTimer = useRef | null>(null); const previousStash = gameStatus?.status.previous_stash ?? null; useEffect(() => { if (!previousStash) return; setLingeredStash(previousStash); if (lingerTimer.current) clearTimeout(lingerTimer.current); lingerTimer.current = setTimeout(() => setLingeredStash(null), TRICK_LINGER_MS); return () => { 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)]); if (!gameStatus || !myPlayer) { return

Načítava sa…

; } const { completed, players, series_number, round_number, cards_in_round, status } = gameStatus; const { active_player, active_round_guesses, active_round_stashes, active_stash, standings = [], standings_guesses = [], } = status; if (completed) { return ; } const myOrder = myPlayer.order; const isPlayPhase = active_stash !== undefined; const myTurnToPlay = isPlayPhase && active_player === myOrder; const activeCards = active_stash ? Object.keys(active_stash.cards).length : 0; const displayedStash: StashData | null = activeCards > 0 && active_stash ? active_stash : lingeredStash ?? null; const playableKeys = myTurnToPlay && active_stash ? computePlayable(hand, active_stash.cards[String(active_stash.first_player)]?.color ?? null) : undefined; const activePlayerName = players.find((p) => p.order === active_player)?.name ?? ''; // Seat mapping relative to "Ty": left / across / right. const seat = (offset: number): PlayerInfo | undefined => players.find((p) => p.order === (myOrder + offset) % 4); const leftP = seat(1); 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; // 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. const completedTricks = (active_round_stashes ?? []).reduce((a, b) => a + b, 0); const cardsInHandOf = (o?: number) => { if (o === undefined) return 0; const playedCurrent = active_stash?.cards[String(o)] ? 1 : 0; return Math.max(0, cards_in_round - completedTricks - playedCurrent); }; const handleLeave = () => leaveGame(navigate); const handleEnd = () => { if (window.confirm('Naozaj ukončiť celú hru pre všetkých?')) { emit.endGame(gameStatus.gid); } }; // The host can always end the game; other players only when the host is // currently offline, so an abandoned game isn't stuck forever waiting for // a host who won't come back, but it isn't open to casual misuse otherwise. const hostConnected = players.find((p) => p.order === 0)?.connected ?? false; const canEnd = myOrder === 0 || !hostConnected; // ── shared pieces ──────────────────────────────────────────────── const bannerText = activeOf(myOrder) ? isPlayPhase ? 'Zahraj kartu' : 'Zadaj tip' : `${activePlayerName} ${isPlayPhase ? 'hrá' : 'tipuje'}`; const banner = (
{bannerText}
); const opponents = players .filter((p) => p.order !== myOrder) .sort((a, b) => a.order - b.order); const totalsRow = (compact: boolean) => (
{opponents.map((p) => (
{p.name}
{computeTotal(standings, p.order)}
))}
{myPlayer.name}
{computeTotal(standings, myOrder)}
); // Center of the oval: trick during play, guess controls during bidding. const ovalContent = isPlayPhase ? ( ) : ( active_round_guesses !== undefined && active_player !== undefined ? ( ) : null ); const topSeat = (
); const sideSeat = (p?: PlayerInfo) => (
); const meSeat = (
); const handArea = ( ); // ── DESKTOP LAYOUT ─────────────────────────────────────────────── if (desktop) { return (
{/* Design canvas — fixed height, width spans the viewport; scaled as one unit so the whole board (and header) zooms with the window. */}
{/* main */}
{/* header */}
Bridžik
Séria {series_number + 1} · Kolo {round_number + 1}
{banner}
{totalsRow(true)}
{canEnd && ( )}
{/* game content — players hug the edges so the felt uses full width */}
{topSeat}
{sideSeat(leftP)}
{ovalContent}
{sideSeat(rightP)}
{meSeat}
{handArea}
{/* sidebar */}
); } // ── MOBILE LAYOUT ──────────────────────────────────────────────── return (
{/* header */}
Séria {series_number + 1} · Kolo {round_number + 1}
{canEnd && ( )}
{totalsRow(false)}
{/* turn banner */}
{banner}
{/* game area */}
{topSeat}
{sideSeat(leftP)}
{ovalContent}
{sideSeat(rightP)}
{meSeat}
{handArea} {/* score */}
); }