Add React frontend and clean up legacy HTTP backend

This commit is contained in:
tim
2026-06-15 22:20:56 +02:00
parent b8e2d15e27
commit beaf142ee4
40 changed files with 8328 additions and 437 deletions
+149
View File
@@ -0,0 +1,149 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useGameStore } from '../store/gameStore';
import { leaveGame } from '../lib/leaveGame';
import { computePlayable } from '../lib/gameRules';
import Hand from '../components/Hand';
import GuessControls from '../components/GuessControls';
import Trick from '../components/Trick';
import Standings from '../components/Standings';
import GameOver from './GameOver';
import type { StashData } from '../types';
const TRICK_LINGER_MS = 3000;
export default function GameTable() {
const navigate = useNavigate();
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<StashData | null>(null);
const lingerTimer = useRef<ReturnType<typeof setTimeout> | 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 <p className="text-center text-gray-400 pt-20">Nacitava sa...</p>;
}
const { completed, players, series_number, round_number, cards_in_round, status } = gameStatus;
const {
active_player,
active_round_guesses,
active_round_stashes,
active_stash,
standings = [],
} = status;
if (completed) {
return <GameOver players={players} standings={standings} />;
}
const myOrder = myPlayer.order;
const isPlayPhase = active_stash !== undefined;
const myTurnToPlay = isPlayPhase && active_player === myOrder;
// Show active stash if it has cards; otherwise show the lingered completed trick.
const activeCards = active_stash ? Object.keys(active_stash.cards).length : 0;
const displayedStash = activeCards > 0 ? active_stash : lingeredStash ?? undefined;
// Highlight only cards the engine would accept
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 ?? '';
const handleLeave = () => leaveGame(navigate);
return (
<div className="max-w-lg mx-auto p-3 flex flex-col gap-3 pb-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="text-sm text-gray-400">
<span>Seria {series_number} / Kolo {round_number + 1}</span>
<span className="ml-2 text-gray-500">({cards_in_round} kopok)</span>
</div>
<button onClick={handleLeave} className="text-xs text-gray-500 hover:text-gray-300">
Odist
</button>
</div>
{/* Turn indicator */}
<div className="bg-slate-800/60 rounded-lg px-3 py-2 text-sm text-center">
{active_player === myOrder ? (
<span className="text-yellow-300 font-semibold">
{!isPlayPhase ? 'Zadaj svoj tip' : 'Zahraj kartu'}
</span>
) : (
<span className="text-gray-400">
Na rade: <span className="text-white">{activePlayerName}</span>
</span>
)}
</div>
{/* Guesses summary (always shown when available) */}
{active_round_guesses && (
<div className="bg-slate-800/60 rounded-lg px-3 py-2">
<p className="text-xs text-gray-400 mb-1">Tipy:</p>
<div className="flex gap-3 flex-wrap">
{players.map((p) => {
const guess = active_round_guesses[String(p.order)];
const wins = active_round_stashes?.[p.order] ?? 0;
return (
<span key={p.order} className="text-sm">
<span className="text-gray-300">{p.name}:</span>{' '}
{guess !== undefined ? (
<span className="text-white font-semibold">
{isPlayPhase ? `${wins}/${guess}` : guess}
</span>
) : (
<span className="text-gray-500">?</span>
)}
</span>
);
})}
</div>
</div>
)}
{/* Active stash (trick) — container always visible during play phase, cards linger 3 s */}
{isPlayPhase && (
<Trick stash={displayedStash ?? null} />
)}
{/* Guess phase controls */}
{!isPlayPhase && active_round_guesses !== undefined && active_player !== undefined && (
<GuessControls
cardsInRound={cards_in_round}
guesses={active_round_guesses}
myOrder={myOrder}
activePlayer={active_player}
activePlayerName={activePlayerName}
/>
)}
{/* Hand */}
<div>
<p className="text-xs text-gray-400 mb-1 text-center">Tvoje karty</p>
<Hand hand={hand} myTurn={myTurnToPlay} isPlayPhase={isPlayPhase} playableKeys={playableKeys} />
</div>
{/* Standings */}
<Standings standings={standings} players={players} />
</div>
);
}