diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx
new file mode 100644
index 0000000..6047c0c
--- /dev/null
+++ b/src/components/Layout/index.tsx
@@ -0,0 +1,21 @@
+import './style.css';
+export default function Layout(props: {
+ left?: React.ReactNode;
+ center?: React.ReactNode;
+ right?: React.ReactNode;
+}) {
+ return (
+
+
+
{props.left}
+
{props.center}
+
{props.right}
+
+
+ );
+}
diff --git a/src/components/Layout/style.css b/src/components/Layout/style.css
new file mode 100644
index 0000000..4794451
--- /dev/null
+++ b/src/components/Layout/style.css
@@ -0,0 +1,28 @@
+.layout-template {
+ display: grid;
+ grid-template-areas:
+ 'center'
+ 'right'
+ 'left';
+}
+
+.layout-left {
+ grid-area: left;
+}
+
+.layout-center {
+ grid-area: center;
+}
+
+.layout-right {
+ grid-area: right;
+}
+
+@media screen and (min-width: 768px) {
+ .layout-template {
+ grid-template-columns: repeat(auto-fit, minmax(min-content, 1fr));
+ justify-items: stretch;
+ align-items: stretch;
+ grid-template-areas: 'left center right';
+ }
+}
diff --git a/src/components/Lobby/index.tsx b/src/components/Lobby/index.tsx
index 33b6f23..991d288 100644
--- a/src/components/Lobby/index.tsx
+++ b/src/components/Lobby/index.tsx
@@ -1,11 +1,31 @@
+import { useAuth } from '~/providers/AuthProvider';
import Board from '../Board';
-import { Option } from './Option';
+import Layout from '../Layout';
+import { Option } from '../GameSelect';
export default function Lobby() {
+ const { user } = useAuth();
return (
-
-
-
-
+
+ }
+ right={
}
+ />
);
}
diff --git a/src/components/Modal/Auth/styles.css b/src/components/Modal/Auth/styles.css
index 3ff0e1c..2df9f76 100644
--- a/src/components/Modal/Auth/styles.css
+++ b/src/components/Modal/Auth/styles.css
@@ -15,6 +15,12 @@
width: 480px;
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
+
+@media (max-width: 480px) {
+ .DialogContent {
+ width: calc(100vw - 32px);
+ }
+}
.DialogContent:focus {
outline: none;
}
@@ -51,4 +57,4 @@
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
-}
\ No newline at end of file
+}
diff --git a/src/components/Modal/Summary/index.tsx b/src/components/Modal/Summary/index.tsx
new file mode 100644
index 0000000..900bc2e
--- /dev/null
+++ b/src/components/Modal/Summary/index.tsx
@@ -0,0 +1,42 @@
+import { Content, Overlay, Portal } from '@radix-ui/react-dialog';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '~/components/UI';
+type Props = {
+ isDraw: boolean;
+ isWinner: boolean;
+ isAborted: boolean;
+ rating: number;
+};
+export default function Summary(props: Props) {
+ const navigate = useNavigate();
+ const title = props.isAborted
+ ? 'Aborted!'
+ : props.isWinner
+ ? 'You Win'
+ : props.isDraw
+ ? 'Draw'
+ : 'You Lose';
+ return (
+
+
+
+
+
+
{title}
+
Rating: {props.rating || 0}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Room/index.tsx b/src/components/Room/index.tsx
index 3223a8f..792faa1 100644
--- a/src/components/Room/index.tsx
+++ b/src/components/Room/index.tsx
@@ -1,203 +1,75 @@
-import { Square } from 'chess.js';
-import cn from 'classnames';
-import { Chessboard } from 'react-chessboard';
-import { BsFillStopwatchFill } from 'react-icons/bs';
-import { FiUser } from 'react-icons/fi';
-import { Bishop, King, Knight, Pawn, Queen, Rook } from '~/assets/icons';
import { useAuth } from '~/providers/AuthProvider';
-import { Square as CustomSquare, customPieces } from '../Board/Elements';
-import { customDarkSquareStyle, customLightSquareStyle } from '../Board/styles';
+import Board from '../Board';
import History from '../History';
+import Layout from '../Layout';
import { ChatRoom } from './ChatRoom';
import useGame, { roomState } from './useGame';
+import { Move } from 'chess.js';
+import Summary from '../Modal/Summary';
+import { Content, Dialog } from '@radix-ui/react-dialog';
-const pieces = {
- wK: () =>
,
- wQ: () =>
,
- wB: () =>
,
- wN: () =>
,
- wR: () =>
,
- wP: () =>
,
- bK: () =>
,
- bQ: () =>
,
- bB: () =>
,
- bN: () =>
,
- bR: () =>
,
- bP: () =>
,
-};
-
-function getPieceKey(piece: string, isWhite: boolean) {
- return isWhite ? `w${piece.toUpperCase()}` : `b${piece.toUpperCase()}`;
-}
-const PiecesCount = {
- k: 1,
- q: 1,
- b: 2,
- n: 2,
- r: 2,
- p: 8,
-};
-
-function getTakenFromFen(fen: string, isWhite: boolean) {
- const position = fen.split(' ')[0];
- const count: { [key: string]: number } = {
- p: 0,
- n: 0,
- b: 0,
- q: 0,
- r: 0,
- k: 0,
- P: 0,
- N: 0,
- B: 0,
- Q: 0,
- R: 0,
- K: 0,
- };
-
- for (let piece of position) {
- if (piece === '/') continue;
- if (isNaN(Number(piece))) {
- count[piece]++;
- }
- }
-
- return {
- p: PiecesCount.p - count[isWhite ? 'p' : 'P'],
- n: PiecesCount.n - count[isWhite ? 'n' : 'N'],
- b: PiecesCount.b - count[isWhite ? 'b' : 'B'],
- q: PiecesCount.q - count[isWhite ? 'q' : 'Q'],
- r: PiecesCount.r - count[isWhite ? 'r' : 'R'],
- k: PiecesCount.k - count[isWhite ? 'k' : 'K'],
- };
-}
-
-const PlayerInfo = (props: {
- picture: string;
- username: string;
- rating: number;
- color: 'white' | 'black';
-}) => {
- return (
-
- {props.picture ? (
-
![]({props.picture})
- ) : (
-
-
-
- )}
-
- {props.username} ({props.rating})
-
-
- );
-};
-
-const PlayerStatus = (props: {
- side: 'top' | 'bottom';
- picture: string;
- username: string;
- rating: number;
- timeLeft: number;
- color: 'white' | 'black';
-}) => {
- const remaining = new Date(0);
- remaining.setMilliseconds(props.timeLeft);
- const timeString = remaining.toISOString().substring(14, 19);
- const fen = roomState.useTrackedStore().fen;
- const takenPieces = getTakenFromFen(fen, props.color === 'white');
- return (
-
-
-
- {Object.entries(takenPieces).map(([piece, count]) => {
- const Piece = (pieces as any)[getPieceKey(piece, props.color !== 'white')];
- return new Array(count).fill(null).map((_, index) =>
);
- })}
-
-
-
- );
-};
+// const pieces = {
+// wK: () =>
,
+// wQ: () =>
,
+// wB: () =>
,
+// wN: () =>
,
+// wR: () =>
,
+// wP: () =>
,
+// bK: () =>
,
+// bQ: () =>
,
+// bB: () =>
,
+// bN: () =>
,
+// bR: () =>
,
+// bP: () =>
,
+// };
const Room = () => {
const { user } = useAuth();
const room = roomState.useTrackedStore();
- const { isLoading, isTurn, isGameOver, move, deselectPiece, select, pieceAtSquare } = useGame();
+ const { isLoading, move } = useGame();
- if (isLoading) return
Loading...
;
+ const onChange = (_fen: string, m: Move) => {
+ move(m.from, m.to);
+ };
- function onSquareClick(square: Square) {
- if (isGameOver()) return;
- if (!isTurn()) return;
- const piece = pieceAtSquare(square);
- const isMove = room.moveFrom && room.moveFrom !== square && room.possibleMoves[square];
- const isSelect = !isMove && piece.color === room.side[0];
- const isDeselect = !isMove && !isSelect && room.moveFrom;
+ if (isLoading) return
Loading...
;
- if (isMove) move(room.moveFrom, square);
- else if (isDeselect) deselectPiece();
- else if (isSelect) select(square);
- }
return (
-
-
-
-
-
+ }
+ center={
+
-
- {
- return piece.startsWith(room.side[0]) && isTurn() && !isGameOver();
- }}
- />
-
- }
+ />
+
-
-
-
+
+
+ >
);
};
diff --git a/src/components/Room/useGame.ts b/src/components/Room/useGame.ts
index be60c6e..36d1132 100644
--- a/src/components/Room/useGame.ts
+++ b/src/components/Room/useGame.ts
@@ -1,45 +1,49 @@
import { useQuery } from '@tanstack/react-query';
import { createStore } from '@udecode/zustood';
import { Chess, Square } from 'chess.js';
-import { CSSProperties, useCallback, useEffect, useRef } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { trpc } from '~/helpers/trpc';
import { GameResponse, GameStatus, Move, Moves } from '~/interfaces';
import { useAuth } from '~/providers/AuthProvider';
import { useSocket } from '~/providers/SocketProvider';
-import { PlayerSide } from '../../interfaces';
-type MoveResponse = { fen: string; moves: Move[]; w_clockms: number; b_clockms: number };
-export const roomState = createStore('room')<{
- side: PlayerSide;
- status: GameStatus;
- moveFrom: string;
- possibleMoves: { [key in Square]?: CSSProperties };
- moves: Moves;
+type MoveResponse = {
fen: string;
- timeLeft: number;
+ moves: Move[];
+ w_clockms: number;
+ b_clockms: number;
+ winner: string;
+};
+type RoomState = {
+ fen: string;
+ moves: Moves;
+ isWinner: boolean;
+ isOpen: boolean;
+ isWhite: boolean;
+ status: GameStatus;
+ player_clockms: number;
+ opponent_clockms: number;
opponent?: {
- username?: string;
- picture?: string;
rating?: number;
- timeLeft: number;
+ picture?: string;
+ username?: string;
};
- winner: string;
-}>(
+};
+export const roomState = createStore('room')
(
{
- side: 'white' as PlayerSide,
- status: 'pending' as GameStatus,
- possibleMoves: {},
- moves: [] as Moves,
- moveFrom: '',
fen: '',
- timeLeft: 0,
+ isOpen: false,
+ player_clockms: 0,
+ moves: [] as Moves,
+ opponent_clockms: 0,
+ isWhite: true,
+ status: 'pending' as GameStatus,
+ isWinner: false,
opponent: {
username: '',
picture: '',
rating: 0,
- timeLeft: 0,
},
- winner: '',
},
{
devtools: {
@@ -68,60 +72,33 @@ export default function useGame() {
return result as GameResponse;
},
onSuccess(data) {
- const side = data.white.uid === user?.uid ? 'white' : 'black';
- const opponent = side === 'white' ? data.black : data.white;
+ const isWhite = data.white.uid === user?.uid;
+ const opponent = isWhite ? data.black : data.white;
chess.load(data.fen);
- const newState = {
- side,
- status: data.status,
- moves: data.moves,
+ const newState: Partial = {
+ isWhite,
fen: data.fen,
- timeLeft: side === 'white' ? data.w_clockms : data.b_clockms,
- opponent: {
- timeLeft: side === 'white' ? data.b_clockms : data.w_clockms,
- },
+ moves: data.moves,
+ status: data.status,
+ isOpen: data.status === 'completed',
+ isWinner: isWhite && data.winner === data.white_id,
+ player_clockms: isWhite ? data.w_clockms : data.b_clockms,
+ opponent_clockms: isWhite ? data.b_clockms : data.w_clockms,
};
opponent &&
Object.assign(newState, {
+ opponent_clockms: isWhite ? data.b_clockms : data.w_clockms,
opponent: {
username: opponent.nickname,
picture: opponent.picture,
rating: opponent.rating,
- timeLeft: opponent.uid === data.black.uid ? data.b_clockms : data.w_clockms,
},
});
roomState.set.mergeState({ ...(newState as any) });
},
});
- useEffect(() => {
- if (!game?.id) return;
- socket.emit('game:join', { data: { gameId: params.gameId } });
- socket.on('opponent:found', refetch);
- socket.on('opponent:joined', refetch);
- socket.on('game:start', refetch);
- socket.on('game:update', ({ fen, b_clockms, w_clockms, moves }: MoveResponse) => {
- roomState.set.mergeState({
- moveFrom: '',
- moves,
- fen,
- possibleMoves: {},
- timeLeft: game.white.uid === user?.uid ? w_clockms : b_clockms,
- opponent: {
- ...roomState.get.opponent?.(),
- timeLeft: game.white.uid === user?.uid ? b_clockms : w_clockms,
- },
- });
- chess.load(game.fen);
- });
- socket.on('game:status', (status: GameStatus) => {
- roomState.set.mergeState({
- status,
- });
- });
- }, [game?.id]);
-
const move = useCallback(async (from: string, to: string) => {
const move = chess.move({ from, to });
if (!move) return;
@@ -130,20 +107,15 @@ export default function useGame() {
result = (await trpc.mutation('game.move', {
move: move.san,
gameId: params.gameId,
- clockms: roomState.get.timeLeft(),
+ clockms: roomState.get.player_clockms(),
})) as MoveResponse;
if (result) {
roomState.set.mergeState({
fen: result.fen,
- moveFrom: '',
moves: result.moves,
- timeLeft: result.w_clockms,
- possibleMoves: {},
- opponent: {
- ...roomState.get.opponent?.(),
- timeLeft: result.b_clockms,
- },
+ player_clockms: result.w_clockms,
+ opponent_clockms: result.b_clockms,
});
}
} catch (e) {
@@ -151,84 +123,55 @@ export default function useGame() {
}
}, []);
- const resign = useCallback(() => {
- socket.emit('game:resign', params.gameId);
- }, []);
-
- const draw = useCallback(() => {
- socket.emit('game:draw', params.gameId);
- }, []);
-
- const isTurn = useCallback(() => {
- const { side, status } = roomState.store.getState();
- return side[0] === chess.turn() && status === 'started';
- }, []);
-
- const possibleMoves = useCallback((square: Square) => {
- return getMoveOptions(chess, square);
+ const onGameUpdate = useCallback(({ fen, b_clockms, w_clockms, moves, winner }: MoveResponse) => {
+ if (!game) return;
+ chess.load(game.fen);
+ const stateUpdate: Partial = {};
+ const isWhite = game.white.uid === user?.uid;
+ if (chess.isGameOver()) {
+ stateUpdate.status = 'completed';
+ stateUpdate.isWinner = isWhite && winner === game.white_id;
+ }
+ stateUpdate.moves = moves;
+ stateUpdate.fen = fen;
+ stateUpdate.player_clockms = isWhite ? w_clockms : b_clockms;
+ stateUpdate.opponent_clockms = isWhite ? b_clockms : w_clockms;
+ roomState.set.mergeState(stateUpdate);
}, []);
- const isGameOver = useCallback(() => {
- return chess.isCheckmate();
+ const resign = useCallback(() => {
+ // socket.emit('game:resign', params.gameId);
}, []);
- const deselectPiece = useCallback(() => {
- roomState.set.mergeState({
- moveFrom: '',
- possibleMoves: {},
- });
+ const draw = useCallback(() => {
+ // socket.emit('game:draw', params.gameId);
}, []);
- const select = useCallback(
- (square: Square) => {
- roomState.set.moveFrom(square);
- roomState.set.possibleMoves(getMoveOptions(chess, square));
- },
- [move, deselectPiece],
- );
-
const pieceAtSquare = useCallback(
(square: Square) => {
return chess.get(square);
},
[chess],
);
+
+ useEffect(() => {
+ if (!game?.id) return;
+ socket.emit('game:join', { data: { gameId: params.gameId } });
+ socket.on('opponent:found', refetch);
+ socket.on('opponent:joined', refetch);
+ socket.on('game:start', refetch);
+ socket.on('game:update', onGameUpdate);
+ socket.on('game:status', (status: GameStatus) => {
+ roomState.set.mergeState({
+ status,
+ });
+ });
+ }, [game?.id]);
return {
move,
draw,
- select,
resign,
- isTurn,
isLoading,
- isGameOver,
- possibleMoves,
pieceAtSquare,
- deselectPiece,
- };
-}
-
-function getMoveOptions(game: Chess, square: Square) {
- const moves = game.moves({
- square,
- verbose: true,
- });
-
- if (moves.length === 0) return {};
-
- const newSquares: { [key in Square]?: CSSProperties } = {};
-
- moves.map((move) => {
- newSquares[move.to] = {
- background:
- game.get(move.to) && game.get(move.to).color !== game.get(square).color
- ? 'radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)'
- : 'radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)',
- borderRadius: '50%',
- };
- return move;
- });
- newSquares[square] = {
- background: 'rgba(255, 255, 0, 0.4)',
};
- return newSquares;
}
diff --git a/src/components/UI/Panel.ui.tsx b/src/components/UI/Panel.ui.tsx
index d3b0a3a..8d69804 100644
--- a/src/components/UI/Panel.ui.tsx
+++ b/src/components/UI/Panel.ui.tsx
@@ -8,7 +8,7 @@ export default function Panel(props: {
}) {
return (
{props.header}
diff --git a/src/index.css b/src/index.css
index 276ebc6..8cf8c42 100644
--- a/src/index.css
+++ b/src/index.css
@@ -46,50 +46,3 @@
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
}
-
-.room {
- flex: 1;
- display: block;
- overflow-y: auto;
-
- padding: 1rem;
-}
-.header {
- position: sticky;
-}
-.board_container {
- flex: 1;
- width: 90vw;
- max-width: 80vh;
- margin: 0 auto;
-}
-
-.gameroom {
- display: grid;
- grid-template-columns: 1fr;
-}
-
-@media screen and (min-width: 768px) {
- .room {
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: stretch;
- gap: 1rem;
- }
-
- .room-grid-template {
- grid-template-columns: minmax(min-content, 348px) min-content minmax(min-content, 348px);
- }
- .header {
- position: sticky;
- }
- .board_container {
- flex: 1;
- max-width: 70vw;
- display: flex;
- flex-direction: column;
- justify-content: center;
- margin: unset;
- }
-}