diff --git a/.eslintrc.js b/.eslintrc.js index c28e798..688ffc4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,62 +1,45 @@ module.exports = { - 'env': { - 'browser': true, - 'es2021': true, - 'node': true + env: { + browser: true, + es2021: true, + node: true, }, - 'extends': [ + extends: [ 'eslint:recommended', 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended' + 'plugin:@typescript-eslint/recommended', ], - 'overrides': [ - ], - 'parser': '@typescript-eslint/parser', - 'parserOptions': { - 'ecmaVersion': 'latest', - 'sourceType': 'module' + overrides: [], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', }, - 'plugins': [ - 'react', - 'eslint-plugin-react-hooks', - '@typescript-eslint', - ], - 'rules': { + plugins: ['react', 'eslint-plugin-react-hooks', '@typescript-eslint'], + rules: { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', - 'no-debugger':'off', + 'no-debugger': 'off', 'keyword-spacing': ['error'], 'space-infix-ops': ['error'], '@typescript-eslint/quotes': [ 'error', 'single', { - 'avoidEscape': true, - 'allowTemplateLiterals': true - } + avoidEscape: true, + allowTemplateLiterals: true, + }, ], 'jsx-quotes': ['error', 'prefer-single'], 'react/jsx-first-prop-new-line': ['error', 'always'], 'object-curly-spacing': ['error', 'always'], - 'react/jsx-max-props-per-line': ['error', { 'maximum': 1, 'when': 'always' }], - 'react/jsx-closing-bracket-location': ['error', 'tag-aligned'], + 'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'always' }], + 'react/jsx-closing-bracket-location': ['error', 'tag-aligned'], 'react/jsx-closing-tag-location': ['error'], 'react/jsx-child-element-spacing': ['error'], - 'indent': [ - 'error', - 2 - ], - 'linebreak-style': [ - 'error', - 'unix' - ], - 'quotes': [ - 'error', - 'single' - ], - 'semi': [ - 'error', - 'never' - ] - } -} + 'indent': ['error', 2], + 'linebreak-style': ['error', 'unix'], + 'quotes': ['error', 'single'], + 'semi': ['error', 'never'], + }, +}; diff --git a/.prettierrc.cjs b/.prettierrc.cjs index 1b15ece..bb8a131 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -1,12 +1,12 @@ module.exports = { printWidth: 100, semi: false, - trailingComma: "es5", + trailingComma: 'es5', useTabs: false, tabWidth: 2, singleQuote: true, - quoteProps: "consistent", + quoteProps: 'consistent', jsxSingleQuote: true, - arrowParens: "always", - endOfLine: "lf", + arrowParens: 'always', + endOfLine: 'lf', }; diff --git a/server/index.ts b/server/index.ts index 686a306..bd453fe 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,4 +1,3 @@ - import express, { Express, NextFunction, Request, Response } from 'express' import session from 'express-session' import path from 'path' @@ -12,7 +11,7 @@ const app: Express = express() const users: Record = {} -const rooms: Record = {} +const rooms: Record = {} const createNewUser = (name: string, id: string) => { users[id] = { @@ -24,17 +23,23 @@ const createNewUser = (name: string, id: string) => { return users[id] } -const createRoom = (name:string) => { +const createRoom = (name: string) => { rooms[name] = {} } -app.use(cors( - { +const broadcastUpdate = (socket: Socket, roomName: string) => { + const room = rooms[roomName] + const serializedRoom = Object.keys(room).map((key) => room[key]) + socket.broadcast.to(roomName).emit('update', { players: serializedRoom }) +} + +app.use( + cors({ origin: isDev && 'http://localhost:3000', methods: ['GET', 'POST'], - credentials: true - } -)) + credentials: true, + }) +) app.use(express.static(path.join(__dirname, '..'))) const sessionMiddleware = session({ secret: 'changeit', @@ -43,7 +48,7 @@ const sessionMiddleware = session({ }) app.use(sessionMiddleware) -app.use(express.json()) +app.use(express.json()) app.get('/*', (_req: Request, res: Response) => { res.sendFile(path.join(__dirname, '..', 'index.html')) }) @@ -65,27 +70,26 @@ const io = new Server(server, { cors: { origin: isDev && 'http://localhost:3000', methods: ['GET', 'POST'], - credentials: true - } + credentials: true, + }, }) io.engine.use(sessionMiddleware) export type Card = { - value?: string, + value?: string } export type Player = { - id: string, - value: string | null - name: string, + id: string + value: string | null + name: string } io.on('connection', (socket) => { const sessionId = socket.request.session.id - - socket.on('mynameis', ({ name }: {name: string}) => { + socket.on('mynameis', ({ name }: { name: string }) => { createNewUser(name, sessionId) socket.emit('mynameis', { name }) }) @@ -98,8 +102,7 @@ io.on('connection', (socket) => { }) }) - - socket.on('join', ({ roomName }: {roomName: string}) => { + socket.on('join', ({ roomName }: { roomName: string }) => { if (!users[sessionId]) { createNewUser('Anonimous', sessionId) } @@ -107,25 +110,39 @@ io.on('connection', (socket) => { if (!rooms[roomName]) { createRoom(roomName) } - const room = rooms[roomName] - room[sessionId] = users[sessionId] + + const room = rooms[roomName] + + const isRoomFull = Object.keys(room).length >= 8 + + if (!isRoomFull) { + room[sessionId] = users[sessionId] + } socket.join(roomName) - const serializedRoom = Object.keys(room).filter((key) => key !== sessionId).map((key) => room[key]) - socket.emit('initial_state', { players: [...serializedRoom] }) + const serializedRoom = Object.keys(room) + .filter((key) => key !== sessionId) + .map((key) => room[key]) + + socket.emit('initial_state', { players: serializedRoom, spectator: isRoomFull, id: sessionId }) - socket.broadcast.to(roomName).emit('new_player', { player: users[sessionId] }) + broadcastUpdate(socket, roomName) }) - socket.on('vote', ({ voteValue }: {voteValue: string}) => { + socket.on('vote', ({ voteValue }: { voteValue: string }) => { socket.rooms.forEach((roomName) => { try { if (rooms[roomName]) { rooms[roomName][sessionId].value = voteValue } - socket.broadcast.to(roomName).emit('player_voted', { value: voteValue, playerId: sessionId }) - } catch { - socket.emit('business_error', { error: 'Internal server error. Your vote was not counted, Please reload the page' }) + socket.broadcast + .to(roomName) + .emit('player_voted', { value: voteValue, playerId: sessionId }) + } catch (e) { + console.log(e) + socket.emit('business_error', { + error: 'Internal server error. Your vote was not counted, Please reload the page', + }) } }) }) @@ -155,13 +172,13 @@ io.on('connection', (socket) => { }) }) - socket.on('leave', ({ roomId }: {roomId: string}) => { - if (rooms[roomId]){ + socket.on('leave', ({ roomId }: { roomId: string }) => { + if (rooms[roomId]) { delete rooms[roomId][sessionId] - socket.broadcast.to(roomId).emit('player_disconnect', { playerId: sessionId }) - if (Object.keys(rooms[roomId]).length === 0) { delete rooms[roomId] + } else { + broadcastUpdate(socket, roomId) } } }) @@ -169,7 +186,7 @@ io.on('connection', (socket) => { socket.on('disconnecting', () => { if (users[sessionId]) { socket.rooms.forEach((roomName) => { - if (rooms[roomName]){ + if (rooms[roomName]) { delete rooms[roomName][sessionId] socket.broadcast.to(roomName).emit('player_disconnect', { playerId: sessionId }) } diff --git a/src/pages/room/features/PlayingDesk/constants.ts b/src/pages/room/features/PlayingDesk/constants.ts index 657d28d..be7ea66 100644 --- a/src/pages/room/features/PlayingDesk/constants.ts +++ b/src/pages/room/features/PlayingDesk/constants.ts @@ -1,9 +1,12 @@ import { SpringValue, SpringValues, UseSpringProps } from '@react-spring/web' -type MappingsType = Record UseSpringProps>> +type MappingsType = Record< + number, + Record UseSpringProps> +> const PLAYER_LABEL_HEIGHT = 56 -const CARD_HEIGHT = 80 +const CARD_HEIGHT = 80 const PLAYER_PLACEMENTS = { UP_MIDDLE: (containerHeight: number, containerWidth: number) => { @@ -12,7 +15,13 @@ const PLAYER_PLACEMENTS = { x: 0, } }, - LEFT_TOP: (containerHeight: number, containerWidth: number)=>{ + BOTTOM_MIDDLE: (containerHeight: number, containerWidth: number) => { + return { + y: containerHeight / 2 + 137 + PLAYER_LABEL_HEIGHT, + x: 0, + } + }, + LEFT_TOP: (containerHeight: number, containerWidth: number) => { return { y: -(containerHeight / 2 + 70), x: -(containerWidth / 2 + 135), @@ -33,19 +42,19 @@ const PLAYER_PLACEMENTS = { RIGHT_MIDDLE: (containerHeight: number, containerWidth: number) => { return { y: 40, - x: (containerWidth / 2 + 195), + x: containerWidth / 2 + 195, } }, - LEFT_BOTTOM: (containerHeight: number, containerWidth: number)=>{ + LEFT_BOTTOM: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + 90 + PLAYER_LABEL_HEIGHT), + y: containerHeight / 2 + 90 + PLAYER_LABEL_HEIGHT, x: -(containerWidth / 2 + 140), } }, RIGHT_BOTTOM: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + 90 + PLAYER_LABEL_HEIGHT), - x: (containerWidth / 2 + 140), + y: containerHeight / 2 + 90 + PLAYER_LABEL_HEIGHT, + x: containerWidth / 2 + 140, } }, LEFT_TOP_30DEG: (containerHeight: number, containerWidth: number) => { @@ -57,41 +66,41 @@ const PLAYER_PLACEMENTS = { RIGHT_TOP_30DEG: (containerHeight: number, containerWidth: number) => { return { y: -(containerHeight / 2 + 30), - x: (containerWidth / 2 + 165), + x: containerWidth / 2 + 165, } }, LEFT_BOTTOM_30DEG: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + 55 + PLAYER_LABEL_HEIGHT), + y: containerHeight / 2 + 55 + PLAYER_LABEL_HEIGHT, x: -(containerWidth / 2 + 165), } }, RIGHT_BOTTOM_30DEG: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + 55 + PLAYER_LABEL_HEIGHT), - x: (containerWidth / 2 + 165), + y: containerHeight / 2 + 55 + PLAYER_LABEL_HEIGHT, + x: containerWidth / 2 + 165, } - } + }, } export const AMOUNT_OF_PLAYERS_TO_INDEX_TO_PLAYER_STYLE_MAP: MappingsType = { 1: { - 0: PLAYER_PLACEMENTS.UP_MIDDLE + 0: PLAYER_PLACEMENTS.UP_MIDDLE, }, 2: { 0: PLAYER_PLACEMENTS.LEFT_TOP, - 1: PLAYER_PLACEMENTS.RIGHT_TOP + 1: PLAYER_PLACEMENTS.RIGHT_TOP, }, 3: { 0: PLAYER_PLACEMENTS.UP_MIDDLE, 1: PLAYER_PLACEMENTS.RIGHT_MIDDLE, - 2: PLAYER_PLACEMENTS.LEFT_MIDDLE + 2: PLAYER_PLACEMENTS.LEFT_MIDDLE, }, 4: { 0: PLAYER_PLACEMENTS.LEFT_TOP, 1: PLAYER_PLACEMENTS.RIGHT_TOP, 2: PLAYER_PLACEMENTS.LEFT_BOTTOM, - 3: PLAYER_PLACEMENTS.RIGHT_BOTTOM, + 3: PLAYER_PLACEMENTS.RIGHT_BOTTOM, }, 5: { 0: PLAYER_PLACEMENTS.LEFT_TOP_30DEG, @@ -106,7 +115,7 @@ export const AMOUNT_OF_PLAYERS_TO_INDEX_TO_PLAYER_STYLE_MAP: MappingsType = { 2: PLAYER_PLACEMENTS.LEFT_BOTTOM, 3: PLAYER_PLACEMENTS.RIGHT_BOTTOM, 4: PLAYER_PLACEMENTS.LEFT_TOP, - 5: PLAYER_PLACEMENTS.RIGHT_TOP + 5: PLAYER_PLACEMENTS.RIGHT_TOP, }, 7: { 0: PLAYER_PLACEMENTS.LEFT_MIDDLE, @@ -116,7 +125,17 @@ export const AMOUNT_OF_PLAYERS_TO_INDEX_TO_PLAYER_STYLE_MAP: MappingsType = { 4: PLAYER_PLACEMENTS.LEFT_TOP, 5: PLAYER_PLACEMENTS.RIGHT_TOP, 6: PLAYER_PLACEMENTS.UP_MIDDLE, - } + }, + 8: { + 0: PLAYER_PLACEMENTS.LEFT_MIDDLE, + 1: PLAYER_PLACEMENTS.RIGHT_MIDDLE, + 2: PLAYER_PLACEMENTS.LEFT_BOTTOM, + 3: PLAYER_PLACEMENTS.RIGHT_BOTTOM, + 4: PLAYER_PLACEMENTS.LEFT_TOP, + 5: PLAYER_PLACEMENTS.RIGHT_TOP, + 6: PLAYER_PLACEMENTS.UP_MIDDLE, + 7: PLAYER_PLACEMENTS.BOTTOM_MIDDLE, + }, } as const export const CARD_PLACEMENTS = { @@ -127,17 +146,17 @@ export const CARD_PLACEMENTS = { rotate: 180, } }, - LEFT_TOP: (containerHeight: number, containerWidth: number)=>{ + LEFT_TOP: (containerHeight: number, containerWidth: number) => { return { - y: -(containerHeight / 2 ), + y: -(containerHeight / 2), x: -(containerWidth / 2 + 45), - rotate: -45 + 180 + rotate: -45 + 180, } }, RIGHT_TOP: (containerHeight: number, containerWidth: number) => { return { y: -(containerHeight / 2), - x: (containerWidth / 2 + 45), + x: containerWidth / 2 + 45, rotate: 45 + 180, } }, @@ -151,35 +170,35 @@ export const CARD_PLACEMENTS = { RIGHT_MIDDLE: (containerHeight: number, containerWidth: number) => { return { y: 40, - x: (containerWidth / 2 + 70), + x: containerWidth / 2 + 70, rotate: -90, } }, - LEFT_BOTTOM: (containerHeight: number, containerWidth: number)=>{ + LEFT_BOTTOM: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + CARD_HEIGHT + 5), + y: containerHeight / 2 + CARD_HEIGHT + 5, x: -(containerWidth / 2 + 40), rotate: 45, } }, RIGHT_BOTTOM: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + CARD_HEIGHT + 5), - x: (containerWidth / 2 + 40), + y: containerHeight / 2 + CARD_HEIGHT + 5, + x: containerWidth / 2 + 40, rotate: -45, } }, LEFT_BOTTOM_30DEG: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + CARD_HEIGHT - 10), + y: containerHeight / 2 + CARD_HEIGHT - 10, x: -(containerWidth / 2 + 55), rotate: 60, } }, RIGHT_BOTTOM_30DEG: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + CARD_HEIGHT - 10), - x: (containerWidth / 2 + 55), + y: containerHeight / 2 + CARD_HEIGHT - 10, + x: containerWidth / 2 + 55, rotate: -60, } }, @@ -193,23 +212,22 @@ export const CARD_PLACEMENTS = { RIGHT_TOP_30DEG: (containerHeight: number, containerWidth: number) => { return { y: -(containerHeight / 2 - 15), - x: (containerWidth / 2 + 55), + x: containerWidth / 2 + 55, rotate: 60 + 180, } }, BOTTOM_MIDDLE: (containerHeight: number, containerWidth: number) => { return { - y: (containerHeight / 2 + 35 + CARD_HEIGHT), + y: containerHeight / 2 + 35 + CARD_HEIGHT, x: 0, rotate: 0, } }, - } export const AMOUNT_OF_PLAYERS_TO_INDEX_TO_CARD_STYLE_MAP: MappingsType = { 1: { - 0: CARD_PLACEMENTS.UP_MIDDLE + 0: CARD_PLACEMENTS.UP_MIDDLE, }, 2: { 0: CARD_PLACEMENTS.LEFT_TOP, @@ -218,13 +236,13 @@ export const AMOUNT_OF_PLAYERS_TO_INDEX_TO_CARD_STYLE_MAP: MappingsType = { 3: { 0: CARD_PLACEMENTS.UP_MIDDLE, 1: CARD_PLACEMENTS.RIGHT_MIDDLE, - 2: CARD_PLACEMENTS.LEFT_MIDDLE + 2: CARD_PLACEMENTS.LEFT_MIDDLE, }, 4: { 0: CARD_PLACEMENTS.LEFT_TOP, 1: CARD_PLACEMENTS.RIGHT_TOP, 2: CARD_PLACEMENTS.LEFT_BOTTOM, - 3: CARD_PLACEMENTS.RIGHT_BOTTOM + 3: CARD_PLACEMENTS.RIGHT_BOTTOM, }, 5: { 0: CARD_PLACEMENTS.LEFT_TOP_30DEG, @@ -249,6 +267,15 @@ export const AMOUNT_OF_PLAYERS_TO_INDEX_TO_CARD_STYLE_MAP: MappingsType = { 4: CARD_PLACEMENTS.LEFT_TOP, 5: CARD_PLACEMENTS.RIGHT_TOP, 6: CARD_PLACEMENTS.UP_MIDDLE, - } + }, + 8: { + 0: CARD_PLACEMENTS.LEFT_MIDDLE, + 1: CARD_PLACEMENTS.RIGHT_MIDDLE, + 2: CARD_PLACEMENTS.LEFT_BOTTOM, + 3: CARD_PLACEMENTS.RIGHT_BOTTOM, + 4: CARD_PLACEMENTS.LEFT_TOP, + 5: CARD_PLACEMENTS.RIGHT_TOP, + 6: CARD_PLACEMENTS.UP_MIDDLE, + 7: CARD_PLACEMENTS.BOTTOM_MIDDLE, + }, } as const - diff --git a/src/pages/room/index.tsx b/src/pages/room/index.tsx index 5ee2eaa..b66503d 100644 --- a/src/pages/room/index.tsx +++ b/src/pages/room/index.tsx @@ -2,21 +2,34 @@ import Sidebar from 'components/Sidebar' import React, { useCallback, useEffect, useState } from 'react' import { DEFAULT_CARD_SYSTEM } from './constants' import PlayingCardsInput from './features/PlayingCardsInput.tsx' +import { useNotifications } from 'hooks/useNotifications' import PlayingDesk from './features/PlayingDesk' -import { Player, PlayerSeialized } from './types' +import { Player } from './types' import { socket } from 'sockets/socket' import { useParams } from 'react-router-dom' import EnterNameModal from 'components/EnterNameModal' import { useLocalStorage } from 'hooks/useLocalStorage' export function Room() { - const { roomName } = useParams<{roomName: string}>() + const { roomName } = useParams<{ roomName: string }>() const [players, setPlayers] = useState([]) const [isRevealed, setIsRevealed] = useState(false) + const [spectatorMode, setSpectatorMode] = useState(false) + const { addNotification } = useNotifications() const [name, setName] = useLocalStorage('userName', null) + const [id, setId] = useLocalStorage('userId', null) const [isNameModalVisible, setIsNameModalVisible] = useState(false) + useEffect(() => { + if (spectatorMode) { + addNotification({ + type: 'error', + text: 'This room is full right now, you joined as a spectator', + }) + } + }, [addNotification, spectatorMode]) + useEffect(() => { if (!name) { setIsNameModalVisible(true) @@ -24,29 +37,27 @@ export function Room() { socket.emit('mynameis', { name }) socket.emit('join', { roomName }) - socket.on('initial_state', ({ players }: {players: PlayerSeialized[]}) => { - const deserializedPlayers = players.map((player) => ({ - name: player.name, - id: player.id, - value: player.value - })) - setPlayers([...deserializedPlayers] ) - }) - - socket.on('new_player', ({ player }: {player: Player}) => { - - setPlayers((prev) => { - return [...prev, player]}) + socket.on( + 'initial_state', + ({ players, id, spectator }: { players: Player[]; spectator: boolean; id: string }) => { + setPlayers(players) + setSpectatorMode(spectator) + setId(id) + } + ) + + socket.on('update', ({ players }: { players: Player[] }) => { + setPlayers(players.filter((player) => player.id !== id)) }) - socket.on('player_disconnect', ({ playerId }: {playerId: string}) => { - setPlayers((prev) => [...prev].filter((player)=> player.id !== playerId)) + socket.on('player_disconnect', ({ playerId }: { playerId: string }) => { + setPlayers((prev) => [...prev].filter((player) => player.id !== playerId)) setIsRevealed(false) }) - socket.on('player_voted', ({ value, playerId }: {value: string, playerId: string}) => { + socket.on('player_voted', ({ value, playerId }: { value: string; playerId: string }) => { setPlayers((prev) => { - return [...prev].map((player)=> player.id === playerId ? { ...player, value } : player) + return [...prev].map((player) => (player.id === playerId ? { ...player, value } : player)) }) }) @@ -61,8 +72,7 @@ export function Room() { }) setUserValue(null) }) - } - + } return () => { socket.emit('leave', { roomId: roomName }) @@ -71,10 +81,13 @@ export function Room() { const [userValue, setUserValue] = useState(null) - const handleUserCardChange = useCallback((value: Player['value']) => { - socket.emit('vote', { voteValue: value }) - setUserValue(value) - }, [setUserValue]) + const handleUserCardChange = useCallback( + (value: Player['value']) => { + socket.emit('vote', { voteValue: value }) + setUserValue(value) + }, + [setUserValue] + ) const handleShowAll = useCallback(() => { socket.emit('show_all') @@ -84,7 +97,6 @@ export function Room() { socket.emit('reset') }, []) - return (
- + {!spectatorMode && ( + + )}
- { setIsNameModalVisible(false) @@ -115,5 +129,4 @@ export function Room() { /> ) - } diff --git a/src/pages/room/types.ts b/src/pages/room/types.ts index 2d0dfbe..c36137c 100644 --- a/src/pages/room/types.ts +++ b/src/pages/room/types.ts @@ -1,11 +1,5 @@ export type Player = { - id: string, - value: string | null - name: string, -} - -export type PlayerSeialized = { - id: string, + id: string value: string | null - name: string, + name: string }