diff --git a/src/components/Board/Elements.tsx b/src/components/Board/Elements.tsx index 1a0a472..76f7490 100644 --- a/src/components/Board/Elements.tsx +++ b/src/components/Board/Elements.tsx @@ -1,7 +1,11 @@ -import { forwardRef } from 'react'; +import cn from 'classnames'; +import { forwardRef, useMemo } from 'react'; import { CustomPieces, CustomSquareRenderer } from 'react-chessboard/dist/chessboard/types'; +import { BsFillStopwatchFill } from 'react-icons/bs'; +import { FiUser } from 'react-icons/fi'; import { Bishop, King, Knight, Pawn, Queen, Rook } from '~/assets/icons'; - +import { getFormatedTime } from './utils'; +export type PiecesTypes = keyof CustomPieces; export const customPieces: CustomPieces = { wB: ({ squareWidth }) => , wN: ({ squareWidth }) => , @@ -17,7 +21,77 @@ export const customPieces: CustomPieces = { bR: ({ squareWidth }) => , }; -export const Square: CustomSquareRenderer = forwardRef(({ children, style }, ref) => { +export const customPiecesMap = new Map(Object.entries(customPieces)); +function getPieceKey(piece: string, isWhite: boolean) { + return isWhite ? `w${piece.toUpperCase()}` : `b${piece.toUpperCase()}`; +} +const PlayerInfo = (props: { picture: string; username: string; rating: number }) => { + return ( +
+ {props.picture ? ( + + ) : ( +
+ +
+ )} +

+ {props.username} ({props.rating}) +

+
+ ); +}; + +type PlayerStatusProps = { + side: 'top' | 'bottom'; + picture: string; + username: string; + rating: number; + timeLeft: number; + isWhite?: boolean; + takenPieces: { + [key: string]: number; + }; +}; + +export const PlayerStatus = (props: PlayerStatusProps) => { + const takenPieces = useMemo( + () => + props.takenPieces + ? Object.entries(props.takenPieces).map(([piece, count]) => { + const Piece = customPiecesMap.get(getPieceKey(piece, !props.isWhite)); + if (!Piece) return null; + return new Array(count) + .fill(null) + .map((_, index) => ( + + )); + }) + : [], + [props.takenPieces], + ); + return ( +
+ +
{takenPieces}
+
+ +

{getFormatedTime(props.timeLeft)}

+
+
+ ); +}; + +export const CustomSquare: CustomSquareRenderer = forwardRef(({ children, style }, ref) => { return (
({ + moveFrom: '', + possibleMoves: {}, +}); + +export default function Board(props: BoardProps) { + const chess = useRef(new Chess(props.fen || START_POSITION)).current; + const board = boardStore.useTrackedStore(); + + useEffect(() => { + chess.load(props.fen || START_POSITION); + }, [props.fen]); + const isTurn = () => (props.isWhite ? chess.turn() === 'w' : chess.turn() === 'b'); -export default function Board(_props: { - width?: number; - disabled?: boolean; - onChange?: (fen: string) => void; -}) { - const game = useRef(new Chess()).current; - - const [moveFrom, setMoveFrom] = useState(''); - const [options, setOptions] = useState<{ [key in Square]?: CSSProperties }>({}); - // const [, setNewFen] = useState(game.fen()); - function getMoveOptions(square: Square) { - const moves = game.moves({ + const isDisabled = () => props.disabled || !isTurn() || chess.isGameOver(); + + function move(from: BoardState['moveFrom'], to: Square) { + const move = chess.move({ + from, + to, + promotion: 'q', + }); + if (!move) return; + boardStore.set.moveFrom(''); + boardStore.set.possibleMoves({}); + props.onChange?.(chess.fen(), move, chess.isGameOver()); + } + + function setPossibleMoves(square: Square) { + const moves = chess.moves({ square, verbose: true, }); @@ -29,7 +66,7 @@ export default function Board(_props: { moves.map((move) => { newSquares[move.to] = { background: - game.get(move.to) && game.get(move.to).color !== game.get(square).color + chess.get(move.to) && chess.get(move.to).color !== chess.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%', @@ -39,69 +76,85 @@ export default function Board(_props: { newSquares[square] = { background: 'rgba(255, 255, 0, 0.4)', }; - return newSquares; + + boardStore.set.possibleMoves(newSquares); + } + + function deselect() { + boardStore.set.moveFrom(''); + boardStore.set.possibleMoves({}); + } + + function select(square: Square) { + boardStore.set.moveFrom(square); + setPossibleMoves(square); } function onSquareClick(square: Square) { - if (_props.disabled) return; - if (moveFrom) { - if (moveFrom === square) { - setMoveFrom(''); - setOptions({}); - return; - } - - const hasOption = options[square]; - - if (hasOption) { - game.move({ - from: moveFrom, - to: square, - }); - setMoveFrom(''); - setOptions({}); - _props.onChange?.(game.fen()); - return; - } else if (game.get(square)) { - setMoveFrom(square); - setOptions(getMoveOptions(square)); - } else { - setMoveFrom(''); - setOptions({}); - } - } - - if (game.get(square)) { - setMoveFrom(square); - setOptions(getMoveOptions(square)); - } + if (isDisabled()) return; + const piece = chess.get(square); + const isMove = board.moveFrom && board.moveFrom !== square && board.possibleMoves[square]; + const isSelect = + !isMove && + ((piece.color === 'w' && props.isWhite) || (piece.color === 'b' && !props.isWhite)); + const isDeselect = !isMove && !isSelect && board.moveFrom; + + if (isMove) move(board.moveFrom, square); + else if (isDeselect) deselect(); + else if (isSelect) select(square); + } + + function onDragOverSquare(_square: Square) { + return !isDisabled(); + } + + function onDrop(_sSquare: Square, _tSquare: Square, _piece: Piece) { + return !isDisabled(); } + + function onDragStart(_piece: Piece, _sourceSquare: Square) { + return !isDisabled(); + } + + function isDraggablePiece() { + return !isDisabled(); + } + + const lostPieces = {}; + const capturedPieces = {}; + return ( -
- { - if (_props.disabled) return false; - if (game.turn() === 'w' && args.piece.startsWith('w')) return true; - else if (game.turn() === 'b' && args.piece.startsWith('b')) return true; - return false; - }} - arePremovesAllowed={false} - customPieces={customPieces} - customSquare={CustomSquare} - customDarkSquareStyle={customDarkSquareStyle} - customLightSquareStyle={customLightSquareStyle} - position={game.fen()} - onSquareClick={onSquareClick} - customSquareStyles={{ - ...options, - }} +
+ +
+ +
+
); diff --git a/src/components/Board/interfaces.ts b/src/components/Board/interfaces.ts new file mode 100644 index 0000000..15ad570 --- /dev/null +++ b/src/components/Board/interfaces.ts @@ -0,0 +1,44 @@ +import { Move, Square } from 'chess.js'; +import { CSSProperties } from 'react'; +import { ChessboardProps } from 'react-chessboard/dist/chessboard/types'; + +export interface SanitizedChessboardProps + extends Partial< + Pick< + ChessboardProps, + | 'id' + | 'position' + | 'customSquare' + | 'customPieces' + | 'isDraggablePiece' + | 'boardOrientation' + | 'areArrowsAllowed' + | 'animationDuration' + | 'arePremovesAllowed' + | 'arePiecesDraggable' + | 'customSquareStyles' + | 'customDarkSquareStyle' + | 'customLightSquareStyle' + > + > {} + +export type BoardProps = { + fen?: string; + isWhite?: boolean; + player?: Player; + opponent?: Player; + disabled?: boolean; + onChange?: (fen: string, move: Move, isGameOver: boolean) => void; +}; + +export type Player = Partial<{ + rating: number; + picture: string; + clockms: number; + username: string; +}>; + +export type BoardState = { + possibleMoves: { [key in Square]?: CSSProperties }; + moveFrom: Square | ''; +}; diff --git a/src/components/Board/styles.css b/src/components/Board/styles.css new file mode 100644 index 0000000..2e62d4d --- /dev/null +++ b/src/components/Board/styles.css @@ -0,0 +1,14 @@ +.board-container { + width: 90vw; + max-width: calc(80vh); + margin: 0 auto; + user-select: none; +} + +@media screen and (min-width: 768px) { + .board-container { + max-width: calc(70vh); + width: 70vw; + margin: 0 auto; + } +} diff --git a/src/components/Board/utils.ts b/src/components/Board/utils.ts new file mode 100644 index 0000000..cbe15b3 --- /dev/null +++ b/src/components/Board/utils.ts @@ -0,0 +1,13 @@ +export function getFormatedTime(time: number) { + const formated = new Date(0); + formated.setMilliseconds(time); + return formated.toISOString().substring(14, 19); +} +export const PiecesCount = { + k: 1, + q: 1, + b: 2, + n: 2, + r: 2, + p: 8, +}; diff --git a/src/components/Lobby/PlayingStatus.tsx b/src/components/GameSelect/PlayingStatus.tsx similarity index 89% rename from src/components/Lobby/PlayingStatus.tsx rename to src/components/GameSelect/PlayingStatus.tsx index 5c95aa5..20bb504 100644 --- a/src/components/Lobby/PlayingStatus.tsx +++ b/src/components/GameSelect/PlayingStatus.tsx @@ -8,10 +8,10 @@ export const PlayingStatus: React.FC = () => { >
-
Player online:
+
Players online:
69420
-
+
Matches ongoing:
4522
diff --git a/src/components/Lobby/Option.tsx b/src/components/GameSelect/index.tsx similarity index 51% rename from src/components/Lobby/Option.tsx rename to src/components/GameSelect/index.tsx index e298d32..7c7476f 100644 --- a/src/components/Lobby/Option.tsx +++ b/src/components/GameSelect/index.tsx @@ -11,6 +11,8 @@ import { appActions } from '~/store'; import { Ticket } from '~/store/lobby.store'; import { TimeItem } from '../UI/TimeItem.ui'; import { PlayingStatus } from './PlayingStatus'; +import Panel from '../UI/Panel.ui'; +import './styles.css'; type CategoryMap = { [key in GameCategory]: (GameMode & { index: number })[] }; const categorize = () => { @@ -66,61 +68,63 @@ export const Option = () => { mutate(); } return ( -
-
-

Play

-

Friends

-
-
-
-
-

Game Mode:

-
- {CATEGORIES.map((category, index) => { - return ( - setState({ ...state, selected_category: category })} - /> - ); - })} + +
+
+

Game Mode:

+
+ {CATEGORIES.map((category, index) => { + return ( + setState({ ...state, selected_category: category })} + /> + ); + })} +
-
-
-

Time:

-
- {categories?.[state.selected_category].map((mode) => { - return ( - setState({ ...state, selected_index: mode.index })} - /> - ); - })} +
+

Time:

+
+ {categories?.[state.selected_category].map((mode) => { + return ( + setState({ ...state, selected_index: mode.index })} + /> + ); + })} +
-
-
-
-
- + } + footer={} + header={ +
+

Play

+

Friends

-
-
+ } + /> ); }; diff --git a/src/components/GameSelect/styles.css b/src/components/GameSelect/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Header.tsx b/src/components/Header/index.tsx similarity index 98% rename from src/components/Header.tsx rename to src/components/Header/index.tsx index e3aad50..41f5b45 100644 --- a/src/components/Header.tsx +++ b/src/components/Header/index.tsx @@ -28,7 +28,7 @@ const Header: FC = () => { appActions.profile.currentTab('profile'); }; return ( -
+
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={