feat(P5B.T5): GameModal — picks board + turn/winner status
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useGame } from '../hooks/useGame';
|
||||
import { ConnectFourBoard } from './ConnectFourBoard';
|
||||
import { TicTacToeBoard } from './TicTacToeBoard';
|
||||
import { XIcon } from './icons';
|
||||
|
||||
interface Props {
|
||||
gameId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function GameModal({ gameId, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { game, loading, error, makeMove } = useGame(gameId);
|
||||
const { session: auth } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose]);
|
||||
|
||||
const myUserId = auth?.user.id ?? null;
|
||||
const myPlayerIdx: 0 | 1 | null =
|
||||
!game || !myUserId
|
||||
? null
|
||||
: game.players[0] === myUserId
|
||||
? 0
|
||||
: game.players[1] === myUserId
|
||||
? 1
|
||||
: null;
|
||||
const isMyTurn = !!game && game.currentTurnUserId === myUserId;
|
||||
const finished = !!game?.finishedAt;
|
||||
const winnerIdx: 0 | 1 | null =
|
||||
!game?.winnerUserId
|
||||
? null
|
||||
: game.players[0] === game.winnerUserId
|
||||
? 0
|
||||
: game.players[1] === game.winnerUserId
|
||||
? 1
|
||||
: null;
|
||||
const title =
|
||||
game?.gameType === 'c4'
|
||||
? t('app:game.c4', { defaultValue: 'Vier-Gewinnt' })
|
||||
: t('app:game.ttt', { defaultValue: 'Tic-Tac-Toe' });
|
||||
|
||||
const statusLine = (() => {
|
||||
if (loading) return t('app:game.loading', { defaultValue: 'Lädt…' });
|
||||
if (error) return error;
|
||||
if (!game) return t('app:game.missing', { defaultValue: 'Spiel nicht gefunden.' });
|
||||
if (finished) {
|
||||
if (winnerIdx === null) return t('app:game.draw', { defaultValue: 'Unentschieden!' });
|
||||
if (winnerIdx === myPlayerIdx) return t('app:game.you_win', { defaultValue: 'Du hast gewonnen!' });
|
||||
return t('app:game.you_lose', { defaultValue: 'Du hast verloren.' });
|
||||
}
|
||||
if (myPlayerIdx === null) return t('app:game.spectator', { defaultValue: 'Du schaust nur zu.' });
|
||||
if (isMyTurn) return t('app:game.your_turn', { defaultValue: 'Du bist dran' });
|
||||
return t('app:game.opponent_turn', { defaultValue: 'Gegner ist dran…' });
|
||||
})();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="fixed inset-0 z-[80] flex flex-col bg-ink-950/95"
|
||||
>
|
||||
<header className="flex shrink-0 items-center justify-between border-b border-line/40 bg-surface-2 px-4 py-2">
|
||||
<h2 className="font-display text-sm font-semibold text-fg">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label={t('app:game.close', { defaultValue: 'Schließen' })}
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-fg-muted hover:bg-surface-3 hover:text-fg"
|
||||
>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto p-6">
|
||||
{game?.gameType === 'ttt' ? (
|
||||
<TicTacToeBoard
|
||||
board={game.state.board}
|
||||
myPlayerIdx={myPlayerIdx}
|
||||
disabled={!isMyTurn || finished}
|
||||
onMove={(cell) => void makeMove({ cell }).catch(() => {})}
|
||||
/>
|
||||
) : game?.gameType === 'c4' ? (
|
||||
<ConnectFourBoard
|
||||
board={game.state.board}
|
||||
myPlayerIdx={myPlayerIdx}
|
||||
disabled={!isMyTurn || finished}
|
||||
onMove={(column) => void makeMove({ column }).catch(() => {})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<footer className="flex shrink-0 items-center justify-center border-t border-line/40 bg-surface-2 px-4 py-3 text-sm font-medium text-fg">
|
||||
{statusLine}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user