Merge pull request #6 from Wenszel/dev
added frontend tests and edited readme
This commit is contained in:
commit
da596e552f
71
README.md
71
README.md
@ -1,13 +1,58 @@
|
||||
# Online multiplayer Ludo
|
||||
# <center>Online Multiplayer Ludo Game</center>
|
||||
|
||||
|
||||
\>\> [Play Online here](www.github.com/wenszel/mern-ludo) <<
|
||||
|
||||
\>\> [Watch YouTube Video here](www.github.com/wenszel/mern-ludo) <<
|
||||
|
||||
## Table of content
|
||||
|
||||
- [About](#about)
|
||||
|
||||
- [Architecture](#architecture)
|
||||
|
||||
- [Key Features and Challenges](#key-features-and-challenges)
|
||||
|
||||
- [Tech Stack](#tech-stack)
|
||||
|
||||
- [Installation](#installation)
|
||||
|
||||
- [Screenshots](#screenshots)
|
||||
|
||||
## About
|
||||
Ludo Online is a multiplayer web-based implementation of the classic board game Ludo, built using the MERN stack and integrated with SocketIO for real-time communication.
|
||||
## Architecture
|
||||

|
||||
## Tech Stack
|
||||
Frontend:
|
||||
  
|
||||
  
|
||||
Backend:
|
||||
   
|
||||
Tests:
|
||||
 
|
||||
Tools:
|
||||
    
|
||||
|
||||
**Ludo** is a strategy board game for two to four players, in which the players race their four tokens from start to finish according to the rolls of a single die. Like other cross and circle games, Ludo is derived from the Indian game Pachisi, but simpler. The game and its variations are popular in many countries and under various names. [Read more](https://en.wikipedia.org/wiki/Ludo_(board_game))
|
||||
## Key Features and Challenges
|
||||
|
||||
- Maintained session consistency with **Express Session** and **MongoDB**.
|
||||
- Enabled real-time communication via **WebSocket** and **SocketIO**.
|
||||
- Ensured code reliability with testing using **Mocha**, **Chai**, and **Jest**.
|
||||
- Hosted in a **Docker** container on **AWS EC2**.
|
||||
- Established CI/CD using **CircleCI**.
|
||||
|
||||
## Interface
|
||||

|
||||
|
||||
## Installation
|
||||
Play this game [here](https://smaga-wiktor-ludo.herokuapp.com)
|
||||
1. Download this repository
|
||||
2. Generate your own [mongoDB atlas](https://www.mongodb.com) credential URL. It should looks like this:
|
||||
|
||||
```
|
||||
mongodb+srv://madmin:<password>@clustername.mongodb.net/<dbname>?retryWrites=true&w=majority
|
||||
```
|
||||
3. Add this URL to the /backend/credentials.js file
|
||||
4. Perform these commands in the main directory:
|
||||
|
||||
```
|
||||
npm i
|
||||
npm start
|
||||
@ -16,16 +61,6 @@ npm i
|
||||
node server.js
|
||||
```
|
||||
|
||||
## Technologies
|
||||
### Backend
|
||||
- Node.js
|
||||
- Express
|
||||
- Express-session
|
||||
- MongoDB, Mongoose
|
||||
- MongoDB sessions store
|
||||
- SocketIO
|
||||
### Frontend
|
||||
- React
|
||||
- Axios
|
||||
- Material UI
|
||||
- Canvas
|
||||
## Screenshots
|
||||
|
||||

|
||||
@ -1,4 +1,4 @@
|
||||
const { getRoom, updateRoom } = require('../controllers/roomController');
|
||||
const { getRoom, updateRoom } = require('../services/roomService');
|
||||
const { sendToPlayersRolledNumber } = require('../socket/emits');
|
||||
const { rollDice, isMoveValid } = require('./handlersFunctions');
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ const rollDice = () => {
|
||||
};
|
||||
|
||||
const makeRandomMove = async roomId => {
|
||||
const { updateRoom, getRoom } = require('../controllers/roomController');
|
||||
const { updateRoom, getRoom } = require('../services/roomService');
|
||||
const room = await getRoom(roomId);
|
||||
if (room.rolledNumber === null) {
|
||||
room.rolledNumber = rollDice();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { getRoom, updateRoom } = require('../controllers/roomController');
|
||||
const { getRoom, updateRoom } = require('../services/roomService');
|
||||
const { colors } = require('../utils/constants');
|
||||
|
||||
module.exports = socket => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../controllers/roomController');
|
||||
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
|
||||
const { sendToOnePlayerRooms, sendToOnePlayerData } = require('../socket/emits');
|
||||
|
||||
module.exports = socket => {
|
||||
@ -16,13 +16,13 @@ module.exports = socket => {
|
||||
};
|
||||
|
||||
const handleGetAllRooms = async () => {
|
||||
let rooms = await getRooms();
|
||||
const rooms = await getRooms();
|
||||
sendToOnePlayerRooms(socket.id, rooms);
|
||||
};
|
||||
|
||||
const handleCreateRoom = async data => {
|
||||
createNewRoom(data);
|
||||
socket.to(socket.id).emit('room:created');
|
||||
sendToOnePlayerRooms(socket.id, await getRooms());
|
||||
};
|
||||
|
||||
socket.on('room:data', handleGetData);
|
||||
|
||||
640
package-lock.json
generated
640
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,6 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.14.20",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"axios": "^1.6.2",
|
||||
"prop-types": "^15.8.1",
|
||||
@ -50,5 +48,9 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:5000"
|
||||
"proxy": "http://localhost:5000",
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { SocketContext } from '../../../App';
|
||||
import one from '../../../images/dice/1.png';
|
||||
import two from '../../../images/dice/2.png';
|
||||
import three from '../../../images/dice/3.png';
|
||||
import four from '../../../images/dice/4.png';
|
||||
import five from '../../../images/dice/5.png';
|
||||
import six from '../../../images/dice/6.png';
|
||||
import roll from '../../../images/dice/roll.png';
|
||||
|
||||
const Dice = ({ rolledNumberCallback, rolledNumber, nowMoving, color, movingPlayer }) => {
|
||||
const socket = useContext(SocketContext);
|
||||
|
||||
const images = [one, two, three, four, five, six, roll];
|
||||
|
||||
const handleRoll = () => {
|
||||
socket.emit('game:roll');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
socket.on('game:roll', number => {
|
||||
rolledNumberCallback(number);
|
||||
});
|
||||
}, [socket, rolledNumberCallback]);
|
||||
|
||||
return (
|
||||
<div className={`dice-container dice-${color}`}>
|
||||
{movingPlayer === color ? (
|
||||
rolledNumber ? (
|
||||
<img src={images[rolledNumber - 1]} alt={rolledNumber} width='100' height='100' />
|
||||
) : nowMoving ? (
|
||||
<img src={images[6]} className='roll' alt='roll' width='100' height='100' onClick={handleRoll} />
|
||||
) : null
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Dice;
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import ReactLoading from 'react-loading';
|
||||
import { PlayerDataContext, SocketContext } from '../../App';
|
||||
import useSocketData from '../../hooks/useSocketData';
|
||||
import Map from './Map/Map';
|
||||
import Navbar from '../Navbar/Navbar';
|
||||
|
||||
@ -11,7 +12,7 @@ const Gameboard = () => {
|
||||
const [pawns, setPawns] = useState([]);
|
||||
const [players, setPlayers] = useState([]);
|
||||
|
||||
const [rolledNumber, setRolledNumber] = useState(null);
|
||||
const [rolledNumber, setRolledNumber] = useSocketData('game:roll');
|
||||
const [time, setTime] = useState();
|
||||
const [isReady, setIsReady] = useState();
|
||||
const [nowMoving, setNowMoving] = useState(false);
|
||||
@ -48,10 +49,6 @@ const Gameboard = () => {
|
||||
});
|
||||
}, [socket]);
|
||||
|
||||
const rolledNumberCallback = number => {
|
||||
setRolledNumber(number);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{(players[0] && !started) || (time && started) ? (
|
||||
@ -64,7 +61,6 @@ const Gameboard = () => {
|
||||
movingPlayer={movingPlayer}
|
||||
rolledNumber={rolledNumber}
|
||||
nowMoving={nowMoving}
|
||||
rolledNumberCallback={rolledNumberCallback}
|
||||
/>
|
||||
<Map pawns={pawns} nowMoving={nowMoving} rolledNumber={rolledNumber} />
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState, useContext } from 'react';
|
||||
import { PlayerDataContext, SocketContext } from '../../../App';
|
||||
|
||||
import mapImage from '../../../images/map.jpg';
|
||||
import positions from '../positions';
|
||||
import positionMapCoords from '../positions';
|
||||
import pawnImages from '../../../constants/pawnImages';
|
||||
import canPawnMove from './canPawnMove';
|
||||
import getPositionAfterMove from './getPositionAfterMove';
|
||||
@ -14,18 +14,19 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
|
||||
const [hintPawn, setHintPawn] = useState();
|
||||
|
||||
const paintPawn = (context, x, y, color) => {
|
||||
const paintPawn = (context, pawn) => {
|
||||
const { x, y } = positionMapCoords[pawn.position];
|
||||
const touchableArea = new Path2D();
|
||||
touchableArea.arc(x, y, 12, 0, 2 * Math.PI);
|
||||
const image = new Image();
|
||||
image.src = pawnImages[color];
|
||||
// image.onload = function () {
|
||||
context.drawImage(image, x - 17, y - 14, 35, 30);
|
||||
image.src = pawnImages[pawn.color];
|
||||
image.onload = function () {
|
||||
context.drawImage(image, x - 17, y - 15, 35, 30);
|
||||
};
|
||||
return touchableArea;
|
||||
};
|
||||
|
||||
const handleCanvasClick = event => {
|
||||
if (hintPawn) {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = canvas.getBoundingClientRect(),
|
||||
@ -33,15 +34,14 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
cursorY = event.clientY - rect.top;
|
||||
for (const pawn of pawns) {
|
||||
if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
|
||||
socket.emit('game:move', pawn._id);
|
||||
if (canPawnMove(pawn, rolledNumber)) socket.emit('game:move', pawn._id);
|
||||
}
|
||||
}
|
||||
setHintPawn(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = event => {
|
||||
if (nowMoving && rolledNumber) {
|
||||
if (!nowMoving || !rolledNumber) return;
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = canvas.getBoundingClientRect(),
|
||||
@ -49,7 +49,6 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
y = event.clientY - rect.top;
|
||||
canvas.style.cursor = 'default';
|
||||
for (const pawn of pawns) {
|
||||
if (pawn.touchableArea) {
|
||||
if (
|
||||
ctx.isPointInPath(pawn.touchableArea, x, y) &&
|
||||
player.color === pawn.color &&
|
||||
@ -58,19 +57,13 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
const pawnPosition = getPositionAfterMove(pawn, rolledNumber);
|
||||
if (pawnPosition) {
|
||||
canvas.style.cursor = 'pointer';
|
||||
if (hintPawn && hintPawn.id === pawn._id) return;
|
||||
setHintPawn({ id: pawn._id, position: pawnPosition, color: 'grey' });
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setHintPawn(null);
|
||||
}
|
||||
} else {
|
||||
setHintPawn(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setHintPawn(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -82,15 +75,10 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||
image.onload = function () {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
pawns.forEach((pawn, index) => {
|
||||
pawns[index].touchableArea = paintPawn(
|
||||
ctx,
|
||||
positions[pawn.position].x,
|
||||
positions[pawn.position].y,
|
||||
pawn.color
|
||||
);
|
||||
pawns[index].touchableArea = paintPawn(ctx, pawn);
|
||||
});
|
||||
if (hintPawn) {
|
||||
paintPawn(ctx, positions[hintPawn.position].x, positions[hintPawn.position].y, hintPawn.color);
|
||||
paintPawn(ctx, hintPawn);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
14
src/components/HOC/withLoading.jsx
Normal file
14
src/components/HOC/withLoading.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import ReactLoading from 'react-loading';
|
||||
|
||||
const withLoading = Component => {
|
||||
return function WithLoading({ isLoading, ...props }) {
|
||||
if (!isLoading) {
|
||||
return <Component {...props} />;
|
||||
}
|
||||
return (
|
||||
<ReactLoading type='spinningBubbles' color='white' height={50} width={50} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default withLoading;
|
||||
@ -1,35 +0,0 @@
|
||||
.refresh {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
border: 1px solid white;
|
||||
}
|
||||
.refresh > img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.private-container {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
input:disabled {
|
||||
background-color: black;
|
||||
color: #999;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
@ -1,56 +1,50 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import './AddServer.css';
|
||||
import React, { useState, useContext } from 'react';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import { SocketContext } from '../../../App';
|
||||
import WindowLayout from '../WindowLayout/WindowLayout';
|
||||
import useInput from '../../../hooks/useInput';
|
||||
import styles from './AddServer.module.css';
|
||||
|
||||
const AddServer = () => {
|
||||
const socket = useContext(SocketContext);
|
||||
const [isPrivate, setIsPrivate] = useState(false);
|
||||
const [serverName, setServerName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
socket.on('room:created', () => {
|
||||
socket.emit('room:rooms');
|
||||
});
|
||||
}, [socket]);
|
||||
const [isIncorrect, setIsIncorrect] = useState(false);
|
||||
const serverName = useInput('');
|
||||
const password = useInput('');
|
||||
|
||||
const handleButtonClick = e => {
|
||||
e.preventDefault();
|
||||
if (!serverName.value) setIsIncorrect(true);
|
||||
else
|
||||
socket.emit('room:create', {
|
||||
name: serverName,
|
||||
name: serverName.value,
|
||||
password: password.value,
|
||||
private: isPrivate,
|
||||
password: password,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='lp-container'>
|
||||
<div className='title-container'>
|
||||
<h1>Host A Server</h1>
|
||||
</div>
|
||||
<div className='content-container'>
|
||||
<form>
|
||||
<WindowLayout
|
||||
title='Host A Server'
|
||||
content={
|
||||
<form className={styles.formContainer}>
|
||||
<input
|
||||
type='text'
|
||||
value={serverName}
|
||||
onChange={e => setServerName(e.target.value)}
|
||||
placeholder='Server Name'
|
||||
{...serverName}
|
||||
style={{
|
||||
border: isIncorrect ? '1px solid red' : '1px solid white',
|
||||
}}
|
||||
/>
|
||||
<div className='private-container'>
|
||||
<p>Private</p>
|
||||
<div className={styles.privateContainer}>
|
||||
<label>Private</label>
|
||||
<Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} />
|
||||
</div>
|
||||
<input
|
||||
type='text'
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder='password'
|
||||
disabled={!isPrivate}
|
||||
/>
|
||||
<input type='text' placeholder='password' disabled={!isPrivate} {...password} />
|
||||
<button onClick={handleButtonClick}>Host</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
15
src/components/LoginPage/AddServer/AddServer.module.css
Normal file
15
src/components/LoginPage/AddServer/AddServer.module.css
Normal file
@ -0,0 +1,15 @@
|
||||
.formContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.privateContainer {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
82
src/components/LoginPage/AddServer/AddServer.test.js
Normal file
82
src/components/LoginPage/AddServer/AddServer.test.js
Normal file
@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { SocketContext } from '../../../App';
|
||||
import AddServer from './AddServer';
|
||||
|
||||
const mockSocket = {
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
describe('AddServer component', () => {
|
||||
it('should renders without crashing', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<AddServer />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
expect(screen.getByText('Host A Server')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handles form submission with valid data when private', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<AddServer />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
|
||||
const serverNameInput = screen.getByPlaceholderText('Server Name');
|
||||
fireEvent.change(serverNameInput, { target: { value: 'Test Server' } });
|
||||
|
||||
const privateSwitch = screen.getByRole('checkbox');
|
||||
fireEvent.click(privateSwitch);
|
||||
|
||||
const passwordInput = screen.getByPlaceholderText('password');
|
||||
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
|
||||
|
||||
const hostButton = screen.getByText('Host');
|
||||
fireEvent.click(hostButton);
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('room:create', {
|
||||
name: 'Test Server',
|
||||
password: 'TestPassword',
|
||||
private: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handles form submission with valid data when not private', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<AddServer />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
|
||||
const serverNameInput = screen.getByPlaceholderText('Server Name');
|
||||
fireEvent.change(serverNameInput, { target: { value: 'Test Server' } });
|
||||
|
||||
const hostButton = screen.getByText('Host');
|
||||
fireEvent.click(hostButton);
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('room:create', {
|
||||
name: 'Test Server',
|
||||
password: '',
|
||||
private: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handles form submission with missing server name', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<AddServer />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
|
||||
const hostButton = screen.getByText('Host');
|
||||
fireEvent.click(hostButton);
|
||||
|
||||
expect(mockSocket.emit).not.toHaveBeenCalled();
|
||||
|
||||
const serverNameInput = screen.getByPlaceholderText('Server Name');
|
||||
expect(serverNameInput).toHaveStyle('border: 1px solid red');
|
||||
});
|
||||
});
|
||||
66
src/components/LoginPage/JoinServer/JoinServer.jsx
Normal file
66
src/components/LoginPage/JoinServer/JoinServer.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { SocketContext } from '../../../App';
|
||||
import refresh from '../../../images/login-page/refresh.png';
|
||||
import NameInput from '../NameInput/NameInput';
|
||||
import Overlay from '../../Overlay/Overlay';
|
||||
import WindowLayout from '../WindowLayout/WindowLayout';
|
||||
import ServersTable from './ServersTable/ServersTable';
|
||||
import withLoading from '../../HOC/withLoading';
|
||||
import useSocketData from '../../../hooks/useSocketData';
|
||||
import styles from './JoinServer.module.css';
|
||||
|
||||
const JoinServer = () => {
|
||||
const socket = useContext(SocketContext);
|
||||
const [rooms, setRooms] = useSocketData('room:rooms');
|
||||
|
||||
const [joining, setJoining] = useState(false);
|
||||
const [clickedRoom, setClickedRoom] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
socket.emit('room:rooms');
|
||||
socket.on('room:rooms', () => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [socket]);
|
||||
|
||||
const getRooms = () => {
|
||||
setRooms([]);
|
||||
socket.emit('room:rooms');
|
||||
};
|
||||
|
||||
const handleJoinClick = room => {
|
||||
setClickedRoom(room);
|
||||
setJoining(true);
|
||||
};
|
||||
|
||||
const ServersTableWithLoading = withLoading(ServersTable);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WindowLayout
|
||||
title='Join A Server'
|
||||
titleComponent={
|
||||
<div className={styles.refresh}>
|
||||
<img src={refresh} alt='refresh' onClick={getRooms} />
|
||||
</div>
|
||||
}
|
||||
content={
|
||||
<div className={styles.serversTableContainer}>
|
||||
<ServersTableWithLoading
|
||||
isLoading={isLoading}
|
||||
rooms={rooms}
|
||||
handleJoinClick={handleJoinClick}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{joining ? (
|
||||
<Overlay handleOverlayClose={() => setJoining(false)}>
|
||||
<NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} />
|
||||
</Overlay>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default JoinServer;
|
||||
20
src/components/LoginPage/JoinServer/JoinServer.module.css
Normal file
20
src/components/LoginPage/JoinServer/JoinServer.module.css
Normal file
@ -0,0 +1,20 @@
|
||||
.serversTableContainer {
|
||||
display: flex;
|
||||
height: 500px;
|
||||
overflow: scroll;
|
||||
width: 100%;
|
||||
}
|
||||
.refresh {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
border: 1px solid white;
|
||||
}
|
||||
.refresh > img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import lock from '../../../../images/login-page/lock.png';
|
||||
import styles from './ServersTable.module.css';
|
||||
|
||||
const ServerListTable = ({ rooms, handleJoinClick }) => {
|
||||
return (
|
||||
<table className={styles.rooms}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.firstColumn}></th>
|
||||
<th>Server</th>
|
||||
<th>#/#</th>
|
||||
<th>Status</th>
|
||||
<th className={styles.lastColumn}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rooms.map((room, index) => (
|
||||
<tr key={index}>
|
||||
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||
<td className={styles.roomName}>{room.name}</td>
|
||||
<td>{`${room.players.length}/4`}</td>
|
||||
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
||||
<td className={styles.lastColumn}>
|
||||
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerListTable;
|
||||
@ -0,0 +1,40 @@
|
||||
.roomName {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-align: left !important;
|
||||
}
|
||||
.rooms > thead > tr :nth-child(2) {
|
||||
text-align: left;
|
||||
}
|
||||
.rooms > tbody > tr > td > img {
|
||||
margin-right: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.rooms > th {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
}
|
||||
.rooms > tbody > tr > td {
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
}
|
||||
.rooms > tbody > tr > td {
|
||||
max-height: 50px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.rooms {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lastColumn {
|
||||
width: 70px;
|
||||
}
|
||||
.firstColumn {
|
||||
width: 40px;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ServersTable from './ServersTable';
|
||||
|
||||
const mockRooms = [
|
||||
{ _id: '1', name: 'Room 1', private: false, players: [], isStarted: false },
|
||||
{ _id: '2', name: 'Room 2', private: true, players: [], isStarted: true },
|
||||
];
|
||||
|
||||
describe('ServersTable component', () => {
|
||||
it('should renders without crashing', () => {
|
||||
render(<ServersTable rooms={mockRooms} handleJoinClick={() => {}} />);
|
||||
expect(screen.getByText('Server')).toBeInTheDocument();
|
||||
expect(screen.getByText('#/#')).toBeInTheDocument();
|
||||
expect(screen.getByText('Status')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should renders the list of rooms', () => {
|
||||
render(<ServersTable rooms={mockRooms} handleJoinClick={() => {}} />);
|
||||
expect(screen.getByText('Room 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Room 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handles join click for each room', () => {
|
||||
const handleJoinClick = jest.fn();
|
||||
render(<ServersTable rooms={mockRooms} handleJoinClick={handleJoinClick} />);
|
||||
|
||||
const joinButtons = screen.getAllByText('Join');
|
||||
fireEvent.click(joinButtons[0]);
|
||||
|
||||
expect(handleJoinClick).toHaveBeenCalledWith(mockRooms[0]);
|
||||
});
|
||||
});
|
||||
@ -1,14 +1,13 @@
|
||||
import './LoginPage.css';
|
||||
import AddServer from './AddServer/AddServer';
|
||||
import ServerList from './ServerList/ServerList';
|
||||
import JoinServer from './JoinServer/JoinServer';
|
||||
import styles from './LoginPage.module.css';
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='login-page-container'>
|
||||
<ServerList />
|
||||
<div className={styles.container}>
|
||||
<JoinServer />
|
||||
<AddServer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
8
src/components/LoginPage/LoginPage.module.css
Normal file
8
src/components/LoginPage/LoginPage.module.css
Normal file
@ -0,0 +1,8 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
19
src/components/LoginPage/LoginPage.test.js
Normal file
19
src/components/LoginPage/LoginPage.test.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import LoginPage from './LoginPage';
|
||||
|
||||
jest.mock('./JoinServer/JoinServer', () => () => <div data-testid="join-server" />);
|
||||
jest.mock('./AddServer/AddServer', () => () => <div data-testid="add-server" />);
|
||||
|
||||
describe('LoginPage component', () => {
|
||||
it('should renders JoinServer component ', () => {
|
||||
render(<LoginPage />);
|
||||
expect(screen.getByTestId('join-server')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should renders AddServer component', () => {
|
||||
render(<LoginPage />);
|
||||
expect(screen.getByTestId('add-server')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -1,70 +0,0 @@
|
||||
.name-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 20px 60px 20px;
|
||||
width: 300px;
|
||||
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
||||
border: 1px solid white;
|
||||
border-radius: 8px;
|
||||
margin: 20px;
|
||||
}
|
||||
.name-input-container > button {
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
align-self: center;
|
||||
}
|
||||
.name-input-container > input {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.name-overlay {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
input,
|
||||
button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
border: 1px solid #ccc;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
color: black;
|
||||
border-color: #4a90e2;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useState, useContext, useEffect, useCallback } from 'react';
|
||||
import { SocketContext } from '../../../App';
|
||||
import useInput from '../../../hooks/useInput';
|
||||
import './NameInput.css';
|
||||
import Overlay from '../../Overlay/Overlay';
|
||||
import useKeyPress from '../../../hooks/useKeyPress';
|
||||
import styles from './NameInput.module.css';
|
||||
|
||||
const NameInput = ({ isRoomPrivate, roomId }) => {
|
||||
const socket = useContext(SocketContext);
|
||||
@ -10,41 +10,31 @@ const NameInput = ({ isRoomPrivate, roomId }) => {
|
||||
const password = useInput('');
|
||||
const [isPasswordWrong, setIsPasswordWrong] = useState(false);
|
||||
|
||||
const handleButtonClick = useCallback(() => {
|
||||
const handleButtonClick = () => {
|
||||
socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId });
|
||||
}, [socket, nickname.value, password.value, roomId]);
|
||||
};
|
||||
|
||||
useKeyPress('Enter', handleButtonClick);
|
||||
|
||||
useEffect(() => {
|
||||
socket.on('error:wrongPassword', () => {
|
||||
setIsPasswordWrong(true);
|
||||
});
|
||||
const keyDownHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleButtonClick();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', keyDownHandler);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}, [socket, handleButtonClick]);
|
||||
}, [socket]);
|
||||
|
||||
return (
|
||||
<div className='name-overlay'>
|
||||
<div className='name-input-container' style={{ height: isRoomPrivate ? '100px' : '50px' }}>
|
||||
<input placeholder='Nickname' type='text' onChange={nickname.onChange} />
|
||||
<div className={styles.container} style={{ height: isRoomPrivate ? '100px' : '50px' }}>
|
||||
<input placeholder='Nickname' type='text' {...nickname} />
|
||||
{isRoomPrivate ? (
|
||||
<input
|
||||
placeholder='Room password'
|
||||
type='text'
|
||||
onChange={password.onChange}
|
||||
{...password}
|
||||
style={{ backgroundColor: isPasswordWrong ? 'red' : null }}
|
||||
/>
|
||||
) : null}
|
||||
<button onClick={handleButtonClick}>JOIN</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
20
src/components/LoginPage/NameInput/NameInput.module.css
Normal file
20
src/components/LoginPage/NameInput/NameInput.module.css
Normal file
@ -0,0 +1,20 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 20px 60px 20px;
|
||||
width: 300px;
|
||||
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
||||
border: 1px solid white;
|
||||
border-radius: 8px;
|
||||
margin: 20px;
|
||||
z-index: 2;
|
||||
}
|
||||
.container > button {
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
align-self: center;
|
||||
}
|
||||
.container > input {
|
||||
margin-top: 10px;
|
||||
}
|
||||
89
src/components/LoginPage/NameInput/NameInput.test.js
Normal file
89
src/components/LoginPage/NameInput/NameInput.test.js
Normal file
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import NameInput from './NameInput';
|
||||
import { SocketContext } from '../../../App';
|
||||
|
||||
const mockSocket = {
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
describe('NameInput component', () => {
|
||||
it('should renders password field when room is private', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<NameInput isRoomPrivate={true} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
expect(screen.getByPlaceholderText('Room password')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not renders password field when room is not private', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<NameInput isRoomPrivate={false} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
expect(screen.queryByPlaceholderText('Room password')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handles input change', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<NameInput isRoomPrivate={false} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
const nicknameInput = screen.getByPlaceholderText('Nickname');
|
||||
fireEvent.change(nicknameInput, { target: { value: 'TestName' } });
|
||||
expect(nicknameInput.value).toBe('TestName');
|
||||
});
|
||||
|
||||
it('should handles password change', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<NameInput isRoomPrivate={true} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
const passwordInput = screen.getByPlaceholderText('Room password');
|
||||
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
|
||||
expect(passwordInput.value).toBe('TestPassword');
|
||||
});
|
||||
|
||||
it('should handles button click', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<NameInput isRoomPrivate={true} roomId={123} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
const nicknameInput = screen.getByPlaceholderText('Nickname');
|
||||
fireEvent.change(nicknameInput, { target: { value: 'TestName' } });
|
||||
const passwordInput = screen.getByPlaceholderText('Room password');
|
||||
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
|
||||
const button = screen.getByText('JOIN');
|
||||
fireEvent.click(button);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('player:login', {
|
||||
name: 'TestName',
|
||||
password: 'TestPassword',
|
||||
roomId: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handles Enter key press', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<NameInput isRoomPrivate={true} roomId={123} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
const nicknameInput = screen.getByPlaceholderText('Nickname');
|
||||
fireEvent.change(nicknameInput, { target: { value: 'TestName' } });
|
||||
const passwordInput = screen.getByPlaceholderText('Room password');
|
||||
fireEvent.change(passwordInput, { target: { value: 'TestPassword' } });
|
||||
fireEvent.keyDown(nicknameInput, { key: 'Enter' });
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('player:login', {
|
||||
name: 'TestName',
|
||||
password: 'TestPassword',
|
||||
roomId: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,51 +0,0 @@
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
img {
|
||||
margin-right: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
height: 50px;
|
||||
}
|
||||
tr {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
.server-container {
|
||||
display: flex;
|
||||
height: 500px;
|
||||
overflow: scroll;
|
||||
}
|
||||
.room-name {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: #ffffff rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { SocketContext } from '../../../App';
|
||||
import lock from '../../../images/login-page/lock.png';
|
||||
import refresh from '../../../images/login-page/refresh.png';
|
||||
import ReactLoading from 'react-loading';
|
||||
|
||||
import './ServerList.css';
|
||||
import NameInput from '../NameInput/NameInput';
|
||||
|
||||
const ServerList = () => {
|
||||
const socket = useContext(SocketContext);
|
||||
const [rooms, setRooms] = useState([]);
|
||||
const [joining, setJoining] = useState(false);
|
||||
const [clickedRoom, setClickedRoom] = useState(null);
|
||||
useEffect(() => {
|
||||
socket.emit('room:rooms');
|
||||
socket.on('room:rooms', data => {
|
||||
data = JSON.parse(data);
|
||||
setRooms(data);
|
||||
});
|
||||
}, [socket]);
|
||||
|
||||
const getRooms = () => {
|
||||
setRooms(null);
|
||||
socket.emit('room:rooms');
|
||||
};
|
||||
|
||||
const handleJoinClick = room => {
|
||||
setClickedRoom(room);
|
||||
setJoining(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='lp-container'>
|
||||
<div className='title-container'>
|
||||
<h1>Server List</h1>
|
||||
<div className='refresh'>
|
||||
<img src={refresh} alt='refresh' onClick={getRooms}></img>
|
||||
</div>
|
||||
</div>
|
||||
<div className='server-container content-container'>
|
||||
{rooms ? (
|
||||
<table className='rooms'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Server</th>
|
||||
<th>#/#</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rooms.map((room, index) => (
|
||||
<tr key={index}>
|
||||
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||
<td className='room-name'>{room.name}</td>
|
||||
<td>{`${room.players.length}/4`}</td>
|
||||
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
||||
<td>
|
||||
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div style={{ alignSelf: 'center' }}>
|
||||
<ReactLoading type='spinningBubbles' color='white' height={50} width={50} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{joining ? <NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ServerList;
|
||||
15
src/components/LoginPage/WindowLayout/WindowLayout.jsx
Normal file
15
src/components/LoginPage/WindowLayout/WindowLayout.jsx
Normal file
@ -0,0 +1,15 @@
|
||||
import styles from './WindowLayout.module.css';
|
||||
|
||||
const WindowLayout = ({ title, titleComponent, content }) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
<h1>{title}</h1>
|
||||
{titleComponent}
|
||||
</div>
|
||||
<div className={styles.content}>{content}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindowLayout;
|
||||
@ -1,25 +1,14 @@
|
||||
.login-page-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lp-container {
|
||||
.container {
|
||||
margin: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 500px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@ -34,17 +23,21 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-container > h1 {
|
||||
.title > h1 {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
border-left: 1px solid black;
|
||||
28
src/components/LoginPage/WindowLayout/WindowLayout.test.js
Normal file
28
src/components/LoginPage/WindowLayout/WindowLayout.test.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import WindowLayout from './WindowLayout';
|
||||
|
||||
jest.mock('./WindowLayout', () => ({ title, titleComponent, content }) => (
|
||||
<div data-testid='mocked-window-layout'>
|
||||
<div data-testid='mocked-title'>{title}</div>
|
||||
<div data-testid='mocked-title-component'>{titleComponent}</div>
|
||||
<div data-testid='mocked-content'>{content}</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
describe('WindowLayout component', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(
|
||||
<WindowLayout
|
||||
title='Test Title'
|
||||
titleComponent={<div>Test Title Component</div>}
|
||||
content={<div>Test Content</div>}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('mocked-window-layout')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('mocked-title')).toHaveTextContent('Test Title');
|
||||
expect(screen.getByTestId('mocked-title-component')).toHaveTextContent('Test Title Component');
|
||||
expect(screen.getByTestId('mocked-content')).toHaveTextContent('Test Content');
|
||||
});
|
||||
});
|
||||
28
src/components/Navbar/Dice/Dice.jsx
Normal file
28
src/components/Navbar/Dice/Dice.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { SocketContext } from '../../../App';
|
||||
import images from '../../../constants/diceImages';
|
||||
import styles from './Dice.module.css';
|
||||
|
||||
const Dice = ({ rolledNumber, nowMoving, playerColor, movingPlayer }) => {
|
||||
const socket = useContext(SocketContext);
|
||||
|
||||
const handleClick = () => {
|
||||
socket.emit('game:roll');
|
||||
};
|
||||
|
||||
const isCurrentPlayer = movingPlayer === playerColor;
|
||||
const hasRolledNumber = rolledNumber !== null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{isCurrentPlayer &&
|
||||
(hasRolledNumber ? (
|
||||
<img src={images[rolledNumber - 1]} alt={rolledNumber} />
|
||||
) : (
|
||||
nowMoving && <img src={images[6]} alt='roll' onClick={handleClick} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dice;
|
||||
11
src/components/Navbar/Dice/Dice.module.css
Normal file
11
src/components/Navbar/Dice/Dice.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.container {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.container > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
74
src/components/Navbar/Dice/Dice.test.js
Normal file
74
src/components/Navbar/Dice/Dice.test.js
Normal file
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import Dice from './Dice';
|
||||
import { SocketContext } from '../../../App';
|
||||
|
||||
const mockSocket = {
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Dice component', () => {
|
||||
let props;
|
||||
const MOVING_PLAYER = 'blue';
|
||||
const NOT_MOVING_PLAYER = 'red';
|
||||
const THIS_PLAYER_MOVING = true;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
rolledNumber: null,
|
||||
nowMoving: false,
|
||||
playerColor: '',
|
||||
movingPlayer: '',
|
||||
};
|
||||
});
|
||||
|
||||
it('should render correct rolledNumber next to moving player', () => {
|
||||
props.rolledNumber = 5;
|
||||
props.movingPlayer = MOVING_PLAYER;
|
||||
props.playerColor = MOVING_PLAYER;
|
||||
render(<Dice {...props} />);
|
||||
expect(screen.queryByAltText(props.rolledNumber)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render rolledNumber next to not moving player', () => {
|
||||
props.rolledNumber = 5;
|
||||
props.movingPlayer = MOVING_PLAYER;
|
||||
props.playerColor = NOT_MOVING_PLAYER;
|
||||
render(<Dice {...props} />);
|
||||
expect(screen.queryByAltText(props.rolledNumber)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render roll icon next to moving player', () => {
|
||||
props.rolledNumber = null;
|
||||
props.movingPlayer = MOVING_PLAYER;
|
||||
props.playerColor = MOVING_PLAYER;
|
||||
props.nowMoving = THIS_PLAYER_MOVING;
|
||||
render(<Dice {...props} />);
|
||||
expect(screen.queryByAltText('roll')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render roll icon next to not moving player', () => {
|
||||
props.rolledNumber = null;
|
||||
props.movingPlayer = MOVING_PLAYER;
|
||||
props.playerColor = MOVING_PLAYER;
|
||||
props.nowMoving = !THIS_PLAYER_MOVING;
|
||||
render(<Dice {...props} />);
|
||||
expect(screen.queryByAltText('roll')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should send data on click', () => {
|
||||
props.rolledNumber = null;
|
||||
props.movingPlayer = MOVING_PLAYER;
|
||||
props.playerColor = MOVING_PLAYER;
|
||||
props.nowMoving = THIS_PLAYER_MOVING;
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<Dice {...props} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
const dice = screen.getByAltText('roll');
|
||||
fireEvent.click(dice);
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('game:roll');
|
||||
});
|
||||
});
|
||||
@ -1,19 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import './TimerAnimation.js';
|
||||
|
||||
const AnimatedOverlay = ({ time }) => {
|
||||
const animationDelay = useMemo(() => 15 - Math.ceil((time - Date.now()) / 1000), [time]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
in={true}
|
||||
timeout={0}
|
||||
classNames='overlay'
|
||||
style={{ animationDelay: `-${animationDelay}s` }}
|
||||
unmountOnExit
|
||||
>
|
||||
<div className='overlay'></div>
|
||||
<CSSTransition in={true} timeout={0} style={{ animationDelay: `-${animationDelay}s` }} unmountOnExit>
|
||||
<div className='overlay' data-testid='animated-overlay'></div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
.overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.9;
|
||||
animation: timerAnimation 15s linear infinite;
|
||||
transition-duration: 15s;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import AnimatedOverlay from './AnimatedOverlay';
|
||||
|
||||
describe('AnimatedOverlay component', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<AnimatedOverlay time={0} />);
|
||||
});
|
||||
|
||||
it('applies animation delay based on time prop', () => {
|
||||
const timeNow = Date.now();
|
||||
const time = timeNow + 5000;
|
||||
render(<AnimatedOverlay time={time} />);
|
||||
const overlay = screen.getByTestId('animated-overlay');
|
||||
const expectedDelay = 15 - Math.ceil((time - timeNow) / 1000);
|
||||
|
||||
expect(overlay).toHaveStyle({ animationDelay: `-${expectedDelay}s` });
|
||||
});
|
||||
});
|
||||
@ -4,20 +4,20 @@ const steps = 86;
|
||||
let count = 0;
|
||||
let s = 'polygon(50% 50%, 50% 0%, 50% 0%';
|
||||
|
||||
for (let i = 50; i <= 100; i += 5) {
|
||||
for (let i = 50; i < 100; i += 5) {
|
||||
s += `, ${i}% 0%`;
|
||||
handle();
|
||||
}
|
||||
for (let i = 0; i <= 100; i += 5) {
|
||||
for (let i = 0; i < 100; i += 5) {
|
||||
s += `, 100% ${i}%`;
|
||||
handle();
|
||||
}
|
||||
for (let i = 100; i >= 0; i -= 5) {
|
||||
for (let i = 100; i > 0; i -= 5) {
|
||||
s += `, ${i}% 100%`;
|
||||
handle();
|
||||
}
|
||||
|
||||
for (let i = 100; i >= 0; i -= 5) {
|
||||
for (let i = 100; i > 0; i -= 5) {
|
||||
s += `, 0% ${i}%`;
|
||||
handle();
|
||||
}
|
||||
@ -52,7 +52,7 @@ function handle() {
|
||||
keyframes.push(step);
|
||||
count++;
|
||||
}
|
||||
|
||||
if (document && document.styleSheets && document.styleSheets[0]) {
|
||||
document.styleSheets[0].insertRule(
|
||||
`
|
||||
@keyframes timerAnimation {
|
||||
@ -61,3 +61,4 @@ document.styleSheets[0].insertRule(
|
||||
`,
|
||||
document.styleSheets[0].cssRules.length
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AnimatedOverlay from './AnimatedOverlay/AnimatedOverlay';
|
||||
import styles from './NameContainer.module.css';
|
||||
|
||||
const NameContainer = ({ player, time }) => {
|
||||
return (
|
||||
<div
|
||||
className='name-container'
|
||||
style={player.ready ? { backgroundColor: player.color } : { backgroundColor: 'lightgrey' }}
|
||||
>
|
||||
<div className={styles.container} style={{ backgroundColor: player.ready ? player.color : 'lightgrey' }}>
|
||||
<p>{player.name}</p>
|
||||
{player.nowMoving ? <AnimatedOverlay time={time} /> : null}
|
||||
</div>
|
||||
@ -17,6 +15,7 @@ const NameContainer = ({ player, time }) => {
|
||||
NameContainer.propTypes = {
|
||||
player: PropTypes.object,
|
||||
time: PropTypes.number,
|
||||
testId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NameContainer;
|
||||
|
||||
13
src/components/Navbar/NameContainer/NameContainer.module.css
Normal file
13
src/components/Navbar/NameContainer/NameContainer.module.css
Normal file
@ -0,0 +1,13 @@
|
||||
.container {
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px solid white;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
58
src/components/Navbar/NameContainer/NameContainer.test.js
Normal file
58
src/components/Navbar/NameContainer/NameContainer.test.js
Normal file
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import NameContainer from './NameContainer';
|
||||
import { NOT_READY_COLOR } from '../../../constants/colors';
|
||||
|
||||
jest.mock('./AnimatedOverlay/AnimatedOverlay.jsx', () => () => {
|
||||
return <mock-animated-overlay data-testid='animated-overlay' />;
|
||||
});
|
||||
|
||||
describe('NameContainer component', () => {
|
||||
let player;
|
||||
let time;
|
||||
|
||||
beforeEach(() => {
|
||||
player = {
|
||||
name: 'TestPlayer',
|
||||
ready: false,
|
||||
color: 'blue',
|
||||
nowMoving: false,
|
||||
};
|
||||
time = 0;
|
||||
});
|
||||
|
||||
it('renders without crashing', () => {
|
||||
render(<NameContainer player={player} time={time} />);
|
||||
});
|
||||
|
||||
it('renders player name', () => {
|
||||
render(<NameContainer player={player} time={time} />);
|
||||
expect(screen.getByText(player.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies grey color when player is not ready', () => {
|
||||
player.ready = false;
|
||||
render(<NameContainer player={player} time={time} testId='name-container' />);
|
||||
const container = screen.getByText(player.name).closest('div');
|
||||
expect(container).toHaveStyle({ backgroundColor: NOT_READY_COLOR });
|
||||
});
|
||||
|
||||
it('applies player colors as background when player is ready', () => {
|
||||
player.ready = true;
|
||||
render(<NameContainer player={player} time={time} testId='name-container' />);
|
||||
const container = screen.getByText(player.name).closest('div');
|
||||
expect(container).toHaveStyle({ backgroundColor: player.color });
|
||||
});
|
||||
|
||||
it('renders AnimatedOverlay when player is nowMoving', () => {
|
||||
const movingPlayer = { ...player, nowMoving: true };
|
||||
render(<NameContainer player={movingPlayer} time={time} />);
|
||||
expect(screen.getByTestId('animated-overlay')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render AnimatedOverlay when player is not nowMoving', () => {
|
||||
render(<NameContainer player={player} time={time} />);
|
||||
expect(screen.queryByTestId('animated-overlay')).toBeNull();
|
||||
});
|
||||
});
|
||||
@ -1,55 +0,0 @@
|
||||
.dice-container {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.roll {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ready-container {
|
||||
display: flex;
|
||||
width: 300px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
flex-flow: row-reverse;
|
||||
background-color: grey;
|
||||
border-radius: 10px;
|
||||
border: 2px solid white;
|
||||
}
|
||||
.ready-container > label {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
color: white;
|
||||
}
|
||||
.player-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
.red {
|
||||
margin-bottom: 50px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.yellow {
|
||||
margin-bottom: 50px;
|
||||
flex-flow: row-reverse;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.blue {
|
||||
margin-top: 50px;
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
.green {
|
||||
margin-top: 50px;
|
||||
flex-flow: row-reverse;
|
||||
grid-column: 2;
|
||||
grid-row: 4;
|
||||
}
|
||||
/* Styl dla overlay */
|
||||
@ -1,29 +1,32 @@
|
||||
import React from 'react';
|
||||
import Dice from '../Gameboard/Dice/Dice';
|
||||
import Dice from './Dice/Dice';
|
||||
import NameContainer from './NameContainer/NameContainer';
|
||||
import ReadyButton from './ReadyButton/ReadyButton';
|
||||
import './Navbar.css';
|
||||
import { PLAYER_COLORS } from '../../constants/colors';
|
||||
import { useContext } from 'react';
|
||||
import { PlayerDataContext } from '../../App';
|
||||
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, rolledNumberCallback, movingPlayer }) => {
|
||||
import styles from './Navbar.module.css';
|
||||
|
||||
const Navbar = ({ players, started, time, isReady, rolledNumber, nowMoving, movingPlayer }) => {
|
||||
const context = useContext(PlayerDataContext);
|
||||
const colors = ['red', 'blue', 'green', 'yellow'];
|
||||
|
||||
const diceProps = {
|
||||
rolledNumber,
|
||||
nowMoving,
|
||||
movingPlayer,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{players.map((player, index) => (
|
||||
<div className={`player-container ${colors[index]}`} key={index}>
|
||||
<div className={`${styles.playerContainer} ${styles[PLAYER_COLORS[index]]}`} key={index}>
|
||||
<NameContainer player={player} time={time} />
|
||||
<Dice
|
||||
movingPlayer={movingPlayer}
|
||||
rolledNumber={rolledNumber}
|
||||
nowMoving={nowMoving}
|
||||
color={colors[index]}
|
||||
rolledNumberCallback={rolledNumberCallback}
|
||||
/>
|
||||
{context.color !== player.color || started ? null : <ReadyButton isReady={isReady} />}
|
||||
{started ? <Dice playerColor={PLAYER_COLORS[index]} {...diceProps} /> : null}
|
||||
{context.color === player.color && !started ? <ReadyButton isReady={isReady} /> : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
||||
28
src/components/Navbar/Navbar.module.css
Normal file
28
src/components/Navbar/Navbar.module.css
Normal file
@ -0,0 +1,28 @@
|
||||
.playerContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
.red {
|
||||
margin-bottom: 50px;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
.yellow {
|
||||
margin-bottom: 50px;
|
||||
flex-flow: row-reverse;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.blue {
|
||||
margin-top: 50px;
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
.green {
|
||||
margin-top: 50px;
|
||||
flex-flow: row-reverse;
|
||||
grid-column: 2;
|
||||
grid-row: 4;
|
||||
}
|
||||
72
src/components/Navbar/Navbar.test.js
Normal file
72
src/components/Navbar/Navbar.test.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import Navbar from './Navbar';
|
||||
import { PlayerDataContext } from '../../App';
|
||||
|
||||
const mockPlayers = [
|
||||
{ name: 'Player1', color: 'red' },
|
||||
{ name: 'Player2', color: 'blue' },
|
||||
];
|
||||
|
||||
const mockPlayerData = {
|
||||
color: 'red',
|
||||
};
|
||||
|
||||
jest.mock('./NameContainer/NameContainer.jsx', () => () => {
|
||||
return <mock-name-container data-testid='name-container' />;
|
||||
});
|
||||
|
||||
jest.mock('./ReadyButton/ReadyButton.jsx', () => () => {
|
||||
return <mock-ready-button data-testid='ready-button' />;
|
||||
});
|
||||
|
||||
jest.mock('./Dice/Dice.jsx', () => () => {
|
||||
return <mock-dice data-testid='dice-container' />;
|
||||
});
|
||||
|
||||
const setup = props => {
|
||||
props.players = mockPlayers;
|
||||
return render(
|
||||
<PlayerDataContext.Provider value={mockPlayerData}>
|
||||
<Navbar {...props} />
|
||||
</PlayerDataContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Navbar component', () => {
|
||||
it('should render NameContainer for each player', () => {
|
||||
setup({
|
||||
started: true,
|
||||
});
|
||||
expect(screen.getAllByTestId('name-container')).toHaveLength(mockPlayers.length);
|
||||
});
|
||||
|
||||
it('should render Dice when started is true', () => {
|
||||
setup({
|
||||
started: true,
|
||||
});
|
||||
expect(screen.getAllByTestId('dice-container')).toHaveLength(mockPlayers.length);
|
||||
});
|
||||
|
||||
it('should not render ReadyButton when started is true', () => {
|
||||
setup({
|
||||
started: true,
|
||||
});
|
||||
expect(screen.queryByTestId('ready-button')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render ReadyButton when started is false', () => {
|
||||
setup({
|
||||
started: false,
|
||||
});
|
||||
expect(screen.getByTestId('ready-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Dice when started is false', () => {
|
||||
setup({
|
||||
started: false,
|
||||
});
|
||||
expect(screen.queryByTestId('dice-container')).toBeNull();
|
||||
});
|
||||
});
|
||||
@ -1,8 +1,7 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { SocketContext } from '../../../App';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import '../Navbar.css';
|
||||
import '../NameContainer/AnimatedOverlay/TimerAnimation';
|
||||
import styles from './ReadyButton.module.css';
|
||||
|
||||
const ReadyButton = ({ isReady }) => {
|
||||
const socket = useContext(SocketContext);
|
||||
@ -13,7 +12,7 @@ const ReadyButton = ({ isReady }) => {
|
||||
setChecked(!checked);
|
||||
};
|
||||
return (
|
||||
<div className='ready-container'>
|
||||
<div className={styles.container}>
|
||||
<Switch onChange={handleCheckboxChange} checked={checked || false} />
|
||||
<label>{checked ? 'I want to play' : 'Im waiting'}</label>
|
||||
</div>
|
||||
|
||||
17
src/components/Navbar/ReadyButton/ReadyButton.module.css
Normal file
17
src/components/Navbar/ReadyButton/ReadyButton.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
flex-direction: column;
|
||||
flex-flow: row-reverse;
|
||||
background-color: grey;
|
||||
border-radius: 10px;
|
||||
border: 2px solid white;
|
||||
}
|
||||
.container > label {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
color: white;
|
||||
}
|
||||
55
src/components/Navbar/ReadyButton/ReadyButton.test.js
Normal file
55
src/components/Navbar/ReadyButton/ReadyButton.test.js
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ReadyButton from './ReadyButton';
|
||||
import { SocketContext } from '../../../App';
|
||||
|
||||
const mockSocket = {
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ReadyButton component', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<ReadyButton isReady={false} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
it('emits "player:ready" event and toggles switch on change', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<ReadyButton isReady={false} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('checkbox');
|
||||
fireEvent.click(switchElement);
|
||||
|
||||
expect(mockSocket.emit).toHaveBeenCalledWith('player:ready');
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
|
||||
it('displays correct label when switch is checked', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<ReadyButton isReady={true} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
|
||||
const labelElement = screen.getByText('I want to play');
|
||||
expect(labelElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays correct label when switch is not checked', () => {
|
||||
render(
|
||||
<SocketContext.Provider value={mockSocket}>
|
||||
<ReadyButton isReady={false} />
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
|
||||
const labelElement = screen.getByText('Im waiting');
|
||||
expect(labelElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
9
src/components/Overlay/Overlay.jsx
Normal file
9
src/components/Overlay/Overlay.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
import styles from './Overlay.module.css';
|
||||
import useKeyPress from '../../hooks/useKeyPress';
|
||||
|
||||
const Overlay = ({ children, handleOverlayClose }) => {
|
||||
useKeyPress('Escape', handleOverlayClose);
|
||||
|
||||
return <div className={styles.container}>{children}</div>;
|
||||
};
|
||||
export default Overlay;
|
||||
13
src/components/Overlay/Overlay.module.css
Normal file
13
src/components/Overlay/Overlay.module.css
Normal file
@ -0,0 +1,13 @@
|
||||
.container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
34
src/components/Overlay/Overlay.test.js
Normal file
34
src/components/Overlay/Overlay.test.js
Normal file
@ -0,0 +1,34 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import Overlay from './Overlay';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Overlay component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders Overlay component', () => {
|
||||
const { container } = render(<Overlay handleOverlayClose={() => {}} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders children inside Overlay', () => {
|
||||
const { getByTestId } = render(
|
||||
<Overlay handleOverlayClose={() => {}}>
|
||||
<div data-testid='test-child' />
|
||||
</Overlay>
|
||||
);
|
||||
expect(getByTestId('test-child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls handleOverlayClose on Escape key press', async () => {
|
||||
const handleOverlayCloseMock = jest.fn();
|
||||
render(<Overlay handleOverlayClose={handleOverlayCloseMock} />);
|
||||
|
||||
await userEvent.type(document.body, '{Escape}');
|
||||
|
||||
expect(handleOverlayCloseMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
2
src/constants/colors.js
Normal file
2
src/constants/colors.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const NOT_READY_COLOR = 'lightgrey';
|
||||
export const PLAYER_COLORS = ['red', 'blue', 'green', 'yellow'];
|
||||
11
src/constants/diceImages.js
Normal file
11
src/constants/diceImages.js
Normal file
@ -0,0 +1,11 @@
|
||||
import one from '../images/dice/1.png';
|
||||
import two from '../images/dice/2.png';
|
||||
import three from '../images/dice/3.png';
|
||||
import four from '../images/dice/4.png';
|
||||
import five from '../images/dice/5.png';
|
||||
import six from '../images/dice/6.png';
|
||||
import roll from '../images/dice/roll.png';
|
||||
|
||||
const diceImages = [one, two, three, four, five, six, roll];
|
||||
|
||||
export default diceImages;
|
||||
@ -1,4 +1,4 @@
|
||||
const positions = [
|
||||
const positionMapCoords = [
|
||||
// Red base
|
||||
{ x: 67, y: 67 }, // 0
|
||||
{ x: 67, y: 116 },
|
||||
@ -114,4 +114,4 @@ const positions = [
|
||||
{ x: 230, y: 200 }, // 91
|
||||
];
|
||||
|
||||
export default positions;
|
||||
export default positionMapCoords;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
export default function useInput({ initialValue }) {
|
||||
export default function useInput(initialValue = '') {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const handleChange = e => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
return {
|
||||
value,
|
||||
value: value,
|
||||
onChange: handleChange,
|
||||
};
|
||||
}
|
||||
|
||||
15
src/hooks/useKeyPress.js
Normal file
15
src/hooks/useKeyPress.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
export default function useKeyPress(targetKey, callback) {
|
||||
const keyPressHandler = ({ key }) => {
|
||||
if (key === targetKey) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', keyPressHandler);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', keyPressHandler);
|
||||
};
|
||||
}, [keyPressHandler]);
|
||||
}
|
||||
19
src/hooks/useSocketData.js
Normal file
19
src/hooks/useSocketData.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { useState, useContext } from 'react';
|
||||
import { SocketContext } from '../App';
|
||||
|
||||
const useSocketData = port => {
|
||||
const socket = useContext(SocketContext);
|
||||
const [data, setData] = useState(null);
|
||||
socket.on(port, res => {
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = JSON.parse(res);
|
||||
} catch (error) {
|
||||
parsedData = res;
|
||||
}
|
||||
setData(parsedData);
|
||||
});
|
||||
return [data, setData];
|
||||
};
|
||||
|
||||
export default useSocketData;
|
||||
BIN
src/images/architecture.png
Normal file
BIN
src/images/architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
@ -20,28 +20,11 @@ canvas {
|
||||
border-radius: 15px;
|
||||
border: 2px solid black;
|
||||
}
|
||||
.dice-container > img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.name-container {
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px solid white;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.timer {
|
||||
background-color: darkblue;
|
||||
color: white;
|
||||
@ -84,3 +67,70 @@ canvas {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2 / span 2;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
border: 1px solid #ccc;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background-color: black;
|
||||
color: #999;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
color: black;
|
||||
border-color: #4a90e2;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: #ffffff rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user