54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { type Cell, tttWinningLine } from '@chat-app/shared/chat';
|
||
|
||
interface Props {
|
||
board: Cell[];
|
||
myPlayerIdx: 0 | 1 | null;
|
||
disabled: boolean;
|
||
onMove: (cell: number) => void;
|
||
}
|
||
|
||
const MARK = ['×', '○'] as const;
|
||
|
||
export function TicTacToeBoard({ board, myPlayerIdx, disabled, onMove }: Props) {
|
||
const winLine = tttWinningLine(board);
|
||
const winSet = new Set<number>(winLine ?? []);
|
||
|
||
return (
|
||
<div
|
||
className="mx-auto grid w-full max-w-md grid-cols-3 gap-2 p-4"
|
||
style={{ aspectRatio: '1 / 1' }}
|
||
role="grid"
|
||
aria-label="Tic-Tac-Toe"
|
||
>
|
||
{board.map((cell, i) => {
|
||
const filled = cell !== null;
|
||
const inWin = winSet.has(i);
|
||
const canClick = !disabled && !filled && myPlayerIdx !== null;
|
||
return (
|
||
<button
|
||
key={i}
|
||
type="button"
|
||
onClick={() => canClick && onMove(i)}
|
||
disabled={!canClick}
|
||
aria-label={'Feld ' + (i + 1) + (filled ? ' belegt' : ' frei')}
|
||
className={
|
||
'flex aspect-square items-center justify-center rounded-xl border-2 text-5xl font-bold transition ' +
|
||
(inWin
|
||
? 'border-emerald-400 bg-emerald-400/20 text-emerald-200'
|
||
: filled
|
||
? cell === 0
|
||
? 'border-rose-500/60 bg-rose-500/10 text-rose-300'
|
||
: 'border-sky-500/60 bg-sky-500/10 text-sky-300'
|
||
: canClick
|
||
? 'cursor-pointer border-line bg-surface-2 text-fg-muted hover:bg-surface-3 hover:text-fg'
|
||
: 'cursor-not-allowed border-line bg-surface-2 text-fg-muted opacity-60')
|
||
}
|
||
>
|
||
{filled ? MARK[cell as 0 | 1] : ''}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|