added tests and css modules to LoginPage components
This commit is contained in:
parent
caf6bc7d91
commit
a33ebeccdd
@ -16,13 +16,13 @@ module.exports = socket => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGetAllRooms = async () => {
|
const handleGetAllRooms = async () => {
|
||||||
let rooms = await getRooms();
|
const rooms = await getRooms();
|
||||||
sendToOnePlayerRooms(socket.id, rooms);
|
sendToOnePlayerRooms(socket.id, rooms);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateRoom = async data => {
|
const handleCreateRoom = async data => {
|
||||||
createNewRoom(data);
|
createNewRoom(data);
|
||||||
socket.to(socket.id).emit('room:created');
|
sendToOnePlayerRooms(socket.id, await getRooms());
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on('room:data', handleGetData);
|
socket.on('room:data', handleGetData);
|
||||||
|
|||||||
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 React, { useState, useContext } from 'react';
|
||||||
import './AddServer.css';
|
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import { SocketContext } from '../../../App';
|
import { SocketContext } from '../../../App';
|
||||||
|
import WindowLayout from '../WindowLayout/WindowLayout';
|
||||||
|
import useInput from '../../../hooks/useInput';
|
||||||
|
import styles from './AddServer.module.css';
|
||||||
|
|
||||||
const AddServer = () => {
|
const AddServer = () => {
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
const [isPrivate, setIsPrivate] = useState(false);
|
const [isPrivate, setIsPrivate] = useState(false);
|
||||||
const [serverName, setServerName] = useState('');
|
const [isIncorrect, setIsIncorrect] = useState(false);
|
||||||
const [password, setPassword] = useState('');
|
const serverName = useInput('');
|
||||||
|
const password = useInput('');
|
||||||
useEffect(() => {
|
|
||||||
socket.on('room:created', () => {
|
|
||||||
socket.emit('room:rooms');
|
|
||||||
});
|
|
||||||
}, [socket]);
|
|
||||||
|
|
||||||
const handleButtonClick = e => {
|
const handleButtonClick = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
socket.emit('room:create', {
|
if (!serverName.value) setIsIncorrect(true);
|
||||||
name: serverName,
|
else
|
||||||
private: isPrivate,
|
socket.emit('room:create', {
|
||||||
password: password,
|
name: serverName.value,
|
||||||
});
|
password: password.value,
|
||||||
|
private: isPrivate,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='lp-container'>
|
<WindowLayout
|
||||||
<div className='title-container'>
|
title='Host A Server'
|
||||||
<h1>Host A Server</h1>
|
content={
|
||||||
</div>
|
<form className={styles.formContainer}>
|
||||||
<div className='content-container'>
|
|
||||||
<form>
|
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
value={serverName}
|
|
||||||
onChange={e => setServerName(e.target.value)}
|
|
||||||
placeholder='Server Name'
|
placeholder='Server Name'
|
||||||
|
{...serverName}
|
||||||
|
style={{
|
||||||
|
border: isIncorrect ? '1px solid red' : '1px solid white',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className='private-container'>
|
<div className={styles.privateContainer}>
|
||||||
<p>Private</p>
|
<label>Private</label>
|
||||||
<Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} />
|
<Switch checked={isPrivate} color='primary' onChange={() => setIsPrivate(!isPrivate)} />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input type='text' placeholder='password' disabled={!isPrivate} {...password} />
|
||||||
type='text'
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
placeholder='password'
|
|
||||||
disabled={!isPrivate}
|
|
||||||
/>
|
|
||||||
<button onClick={handleButtonClick}>Host</button>
|
<button onClick={handleButtonClick}>Host</button>
|
||||||
</form>
|
</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 AddServer from './AddServer/AddServer';
|
||||||
import ServerList from './ServerList/ServerList';
|
import JoinServer from './JoinServer/JoinServer';
|
||||||
|
import styles from './LoginPage.module.css';
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.container}>
|
||||||
<div className='login-page-container'>
|
<JoinServer />
|
||||||
<ServerList />
|
<AddServer />
|
||||||
<AddServer />
|
</div>
|
||||||
</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,59 +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;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.name-input-container > button {
|
|
||||||
margin-top: 5px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100px;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
.name-input-container > input {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
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 React, { useState, useContext, useEffect, useCallback } from 'react';
|
||||||
import { SocketContext } from '../../../App';
|
import { SocketContext } from '../../../App';
|
||||||
import useInput from '../../../hooks/useInput';
|
import useInput from '../../../hooks/useInput';
|
||||||
import './NameInput.css';
|
|
||||||
import useKeyPress from '../../../hooks/useKeyPress';
|
import useKeyPress from '../../../hooks/useKeyPress';
|
||||||
|
import styles from './NameInput.module.css';
|
||||||
|
|
||||||
const NameInput = ({ isRoomPrivate, roomId }) => {
|
const NameInput = ({ isRoomPrivate, roomId }) => {
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
@ -10,11 +10,12 @@ const NameInput = ({ isRoomPrivate, roomId }) => {
|
|||||||
const password = useInput('');
|
const password = useInput('');
|
||||||
const [isPasswordWrong, setIsPasswordWrong] = useState(false);
|
const [isPasswordWrong, setIsPasswordWrong] = useState(false);
|
||||||
|
|
||||||
const handleButtonClick = useCallback(() => {
|
const handleButtonClick = () => {
|
||||||
socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId });
|
socket.emit('player:login', { name: nickname.value, password: password.value, roomId: roomId });
|
||||||
}, [socket, nickname.value, password.value, roomId]);
|
};
|
||||||
|
|
||||||
useKeyPress('Enter', handleButtonClick);
|
useKeyPress('Enter', handleButtonClick);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.on('error:wrongPassword', () => {
|
socket.on('error:wrongPassword', () => {
|
||||||
setIsPasswordWrong(true);
|
setIsPasswordWrong(true);
|
||||||
@ -22,13 +23,13 @@ const NameInput = ({ isRoomPrivate, roomId }) => {
|
|||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='name-input-container' style={{ height: isRoomPrivate ? '100px' : '50px' }}>
|
<div className={styles.container} style={{ height: isRoomPrivate ? '100px' : '50px' }}>
|
||||||
<input placeholder='Nickname' type='text' onChange={nickname.onChange} />
|
<input placeholder='Nickname' type='text' {...nickname} />
|
||||||
{isRoomPrivate ? (
|
{isRoomPrivate ? (
|
||||||
<input
|
<input
|
||||||
placeholder='Room password'
|
placeholder='Room password'
|
||||||
type='text'
|
type='text'
|
||||||
onChange={password.onChange}
|
{...password}
|
||||||
style={{ backgroundColor: isPasswordWrong ? 'red' : null }}
|
style={{ backgroundColor: isPasswordWrong ? 'red' : null }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
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,82 +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';
|
|
||||||
import Overlay from '../../Overlay/Overlay';
|
|
||||||
|
|
||||||
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 ? (
|
|
||||||
<Overlay handleOverlayClose={() => setJoining(false)}>
|
|
||||||
<NameInput roomId={clickedRoom._id} isRoomPrivate={clickedRoom.private} />
|
|
||||||
</Overlay>
|
|
||||||
) : 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 {
|
.container {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
height: 50%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lp-container {
|
|
||||||
margin: 50px;
|
margin: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
padding: 20px;
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-container {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -34,17 +23,21 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-container > h1 {
|
.title > h1 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-top: 10px;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
border-left: 1px solid black;
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
export default function useInput({ initialValue }) {
|
export default function useInput(initialValue = '') {
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
setValue(e.target.value);
|
setValue(e.target.value);
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
value,
|
value: value,
|
||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,5 +11,5 @@ export default function useKeyPress(targetKey, callback) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', keyPressHandler);
|
window.removeEventListener('keydown', keyPressHandler);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [keyPressHandler]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,14 @@ import { SocketContext } from '../App';
|
|||||||
const useSocketData = port => {
|
const useSocketData = port => {
|
||||||
const socket = useContext(SocketContext);
|
const socket = useContext(SocketContext);
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
socket.on(port, data => {
|
socket.on(port, res => {
|
||||||
setData(data);
|
let parsedData;
|
||||||
|
try {
|
||||||
|
parsedData = JSON.parse(res);
|
||||||
|
} catch (error) {
|
||||||
|
parsedData = res;
|
||||||
|
}
|
||||||
|
setData(parsedData);
|
||||||
});
|
});
|
||||||
return [data, setData];
|
return [data, setData];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -67,3 +67,70 @@ canvas {
|
|||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row: 2 / 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