From a2ed634558f00b35b975461de2daaf1461b188ed Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Mon, 14 Aug 2023 02:50:46 -0400 Subject: [PATCH] edit names --- .../front_end_components/Confirmation.js | 4 +- .../front_end_components/HTMLFragments.js | 29 ++++----- .../game_creation/GameCreationStepManager.js | 8 +-- client/src/modules/game_state/states/Ended.js | 1 + .../modules/game_state/states/InProgress.js | 1 + client/src/modules/game_state/states/Lobby.js | 3 +- .../states/shared/SharedStateUtil.js | 33 ++++++++-- .../src/modules/page_handlers/gameHandler.js | 16 ++++- client/src/scripts/join.js | 4 +- client/src/styles/game.css | 62 +++++++++++++++++-- server/config/globals.js | 4 +- server/modules/Events.js | 38 +++++++++++- server/modules/singletons/GameManager.js | 12 ++-- .../modules/singletons/GameManager_Spec.js | 2 +- 14 files changed, 172 insertions(+), 45 deletions(-) diff --git a/client/src/modules/front_end_components/Confirmation.js b/client/src/modules/front_end_components/Confirmation.js index bd1c51c..18a83bd 100644 --- a/client/src/modules/front_end_components/Confirmation.js +++ b/client/src/modules/front_end_components/Confirmation.js @@ -1,6 +1,6 @@ import { toast } from './Toast.js'; -export const Confirmation = (message, onYes = null, isDOMNode = false) => { +export const Confirmation = (message, onYes = null, isDOMNode = false, confirmButtonText = 'Yes') => { document.querySelector('#confirmation')?.remove(); document.querySelector('#confirmation-background')?.remove(); @@ -11,7 +11,7 @@ export const Confirmation = (message, onYes = null, isDOMNode = false) => { ? `
- +
` : `
diff --git a/client/src/modules/front_end_components/HTMLFragments.js b/client/src/modules/front_end_components/HTMLFragments.js index 1e1b44e..09f9c2e 100644 --- a/client/src/modules/front_end_components/HTMLFragments.js +++ b/client/src/modules/front_end_components/HTMLFragments.js @@ -94,7 +94,7 @@ export const HTMLFragments = {
-
+
@@ -128,7 +128,7 @@ export const HTMLFragments = {
-
+
@@ -245,7 +245,7 @@ export const HTMLFragments = {
`, MODERATOR_PLAYER: `
-
+
@@ -254,7 +254,7 @@ export const HTMLFragments = {
`, GAME_PLAYER: `
-
+
`, INITIAL_GAME_DOM: @@ -265,6 +265,9 @@ export const HTMLFragments = {
+
`, // via https://loading.io/css/ @@ -283,18 +286,10 @@ export const HTMLFragments = {
`, - NAME_CHANGE_MODAL: - ` -
-
+
diff --git a/client/src/modules/game_creation/GameCreationStepManager.js b/client/src/modules/game_creation/GameCreationStepManager.js index 3fa2cb8..67133f9 100644 --- a/client/src/modules/game_creation/GameCreationStepManager.js +++ b/client/src/modules/game_creation/GameCreationStepManager.js @@ -98,7 +98,7 @@ export class GameCreationStepManager { this.incrementStep(); this.renderStep('creation-step-container', this.step); } else { - toast('Name must be between 1 and 30 characters.', 'error', true); + toast('Name must be between 1 and 40 characters.', 'error', true); } } }, @@ -569,8 +569,8 @@ function initializeRemainingEventListeners (deckManager, roleBox) { } function processNewCustomRoleSubmission (name, description, team, deckManager, isUpdate, roleBox, option = null) { - if (name.length > 40) { - toast('Your name is too long (max 40 characters).', 'error', true); + if (name.length > 50) { + toast('Your name is too long (max 50 characters).', 'error', true); return; } if (description.length > 500) { @@ -596,5 +596,5 @@ function hasTimer (hours, minutes) { } function validateName (name) { - return typeof name === 'string' && name.length > 0 && name.length <= 30; + return typeof name === 'string' && name.length > 0 && name.length <= 40; } diff --git a/client/src/modules/game_state/states/Ended.js b/client/src/modules/game_state/states/Ended.js index 2a94d8d..40cc29d 100644 --- a/client/src/modules/game_state/states/Ended.js +++ b/client/src/modules/game_state/states/Ended.js @@ -53,6 +53,7 @@ function renderGroupOfPlayers ( for (const player of people) { const playerEl = document.createElement('div'); playerEl.classList.add('game-player'); + playerEl.dataset.pointer = player.id; playerEl.innerHTML = HTMLFragments.GAME_PLAYER; playerEl.querySelector('.game-player-name').innerText = player.name; diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index 1601f3d..1a25180 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -322,6 +322,7 @@ export class InProgress { for (const player of people) { const playerEl = document.createElement('div'); playerEl.classList.add('game-player'); + playerEl.dataset.pointer = player.id; // add a reference to the player's id for each corresponding element in the list if (moderatorType) { diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index bcd3ba8..92e878c 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -354,8 +354,9 @@ function getTimeString (gameState) { function renderLobbyPerson (person, gameState, socket) { const el = document.createElement('div'); + el.dataset.pointer = person.id; const personNameEl = document.createElement('div'); - personNameEl.classList.add('lobby-player-name'); + personNameEl.classList.add('lobby-player-name', 'person-name-element'); const personTypeEl = document.createElement('div'); personNameEl.innerText = person.name; personTypeEl.innerText = person.userType + USER_TYPE_ICONS[person.userType]; diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js index 893d83c..a3db71f 100644 --- a/client/src/modules/game_state/states/shared/SharedStateUtil.js +++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js @@ -100,8 +100,9 @@ export const SharedStateUtil = { } else { for (const spectator of spectators) { const spectatorEl = document.createElement('div'); + spectatorEl.dataset.pointer = spectator.id; spectatorEl.classList.add('spectator'); - spectatorEl.innerHTML = '
' + + spectatorEl.innerHTML = '
' + '
' + 'spectator' + USER_TYPE_ICONS.spectator + '
'; spectatorEl.querySelector('.spectator-name').innerText = spectator.name; list.appendChild(spectatorEl); @@ -123,6 +124,7 @@ export const SharedStateUtil = { }, displayCurrentModerator: (moderator) => { + document.getElementById('current-moderator').dataset.pointer = moderator.id; document.getElementById('current-moderator-name').innerText = moderator.name; document.getElementById('current-moderator-type').innerText = moderator.userType + USER_TYPE_ICONS[moderator.userType]; }, @@ -183,9 +185,30 @@ export const SharedStateUtil = { }); }, - displayClientInfo: (name, userType) => { - document.getElementById('client-name').innerText = name; - document.getElementById('client-user-type').innerText = userType; - document.getElementById('client-user-type').innerText += USER_TYPE_ICONS[userType]; + displayClientInfo: (gameState, socket) => { + document.getElementById('client-name').innerText = gameState.client.name; + document.getElementById('client-user-type').innerText = gameState.client.userType; + document.getElementById('client-user-type').innerText += USER_TYPE_ICONS[gameState.client.userType]; + const nameForm = document.createElement('form'); + nameForm.setAttribute('id', 'name-change-form'); + nameForm.onsubmit = (e) => { + e.preventDefault(); + document.getElementById('confirmation-yes-button').click(); + }; + nameForm.innerHTML = HTMLFragments.NAME_CHANGE_FORM; + nameForm.querySelector('#client-new-name').value = gameState.client.name; + document.getElementById('edit-name-button').addEventListener('click', () => { + Confirmation(nameForm, () => { + socket.emit( + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.CHANGE_NAME, + gameState.accessCode, + { personId: gameState.client.id, newName: document.getElementById('client-new-name').value }, + (response) => { + toast(response.message, response.errorFlag === 1 ? 'error' : 'success', true); + } + ); + }, true, 'Update'); + }); } }; diff --git a/client/src/modules/page_handlers/gameHandler.js b/client/src/modules/page_handlers/gameHandler.js index 8b6c923..a2445af 100644 --- a/client/src/modules/page_handlers/gameHandler.js +++ b/client/src/modules/page_handlers/gameHandler.js @@ -135,7 +135,7 @@ function processGameState ( }); } - SharedStateUtil.displayClientInfo(currentGameState.client.name, currentGameState.client.userType); + SharedStateUtil.displayClientInfo(currentGameState, socket); switch (currentGameState.status) { case STATUS.LOBBY: @@ -236,6 +236,20 @@ function setClientSocketHandlers (stateBucket, socket) { ); }); + socket.on(EVENT_IDS.CHANGE_NAME, (changedId, newName) => { + const person = stateBucket.currentGameState.people.find(person => person.id === changedId); + if (person) { + person.name = newName; + if (stateBucket.currentGameState.client.id === changedId) { + stateBucket.currentGameState.client.name = newName; + SharedStateUtil.displayClientInfo(stateBucket.currentGameState, socket); + } + document.querySelectorAll('[data-pointer="' + person.id + '"]').forEach((node) => { + node.querySelector('.person-name-element').innerText = newName; + }); + } + }); + socket.on(EVENT_IDS.END_GAME, (people) => { stateBucket.currentGameState.people = people; stateBucket.currentGameState.status = STATUS.ENDED; diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index e5e1175..452f9c1 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -55,7 +55,7 @@ const joinHandler = (e) => { resetJoinButtonState(e, joinHandler); }); } else { - toast('Name must be between 1 and 30 characters.', 'error', true, true, 'long'); + toast('Name must be between 1 and 40 characters.', 'error', true, true, 'long'); } }; @@ -90,7 +90,7 @@ function resetJoinButtonState (e, joinHandler) { } function validateName (name) { - return typeof name === 'string' && name.length > 0 && name.length <= 30; + return typeof name === 'string' && name.length > 0 && name.length <= 40; } if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { diff --git a/client/src/styles/game.css b/client/src/styles/game.css index 1dadd42..c200c22 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -564,8 +564,34 @@ h1 { } #client-container { - max-width: 35em; + max-width: 25em; + width: 75%; margin: 1em 0; + position: relative; +} + +#client-container button { + background-color: transparent; + height: fit-content; + margin: 0 8px; + cursor: pointer; + padding: 5px; + border-radius: 5px; + border: 1px solid transparent; + position: absolute; + right: -50px; + top: 32px; +} + +#client-container button:hover { + cursor: pointer; + filter: brightness(1.5); + background-color: #8080804d; +} + +#client-container button img { + width: 22px; + pointer-events: none; } #client { @@ -575,16 +601,19 @@ h1 { align-items: center; justify-content: space-between; border-radius: 5px; - min-width: 15em; border: 1px solid #46455299; background: #4645525c; } #client-name { + max-width: 13em; color: #e7e7e7; font-family: 'signika-negative', sans-serif; font-size: 25px; margin: 0.25em 2em 0.25em 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } #client-user-type { @@ -920,8 +949,22 @@ canvas { justify-content: center; } -#change-name-modal-background { - cursor: default; +#change-name-form-content { + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 15em; + text-align: left; + margin: 2px auto; +} + +#change-name-form-content input { + font-size: 20px; +} + +#change-name-form-content label { + display: flex; + margin-bottom: 0.5em; } #lobby-people-container , #game-people-container { @@ -954,6 +997,16 @@ canvas { } @media(max-width: 500px) { + #client-container button img { + width: 18px; + pointer-events: none; + } + + #client-container button { + right: -46px; + top: 27px; + } + label { font-size: 18px; } @@ -968,6 +1021,7 @@ canvas { #client-name { font-size: 20px; + margin: 0.25em 0 0.25em 0; } #client-user-type, #game-parameters { diff --git a/server/config/globals.js b/server/config/globals.js index 29ebc14..e49bbe2 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -10,7 +10,8 @@ const PRIMITIVES = { USER_SIGNATURE_LENGTH: 25, INSTANCE_ID_LENGTH: 75, MAX_SPECTATORS: 25, - MOCK_AUTH: 'mock_auth' + MOCK_AUTH: 'mock_auth', + MAX_PERSON_NAME_LENGTH: 40 }; const LOG_LEVEL = { @@ -88,6 +89,7 @@ const SYNCABLE_EVENTS = function () { EVENT_IDS.KILL_PLAYER, EVENT_IDS.REVEAL_PLAYER, EVENT_IDS.TRANSFER_MODERATOR, + EVENT_IDS.CHANGE_NAME, EVENT_IDS.END_GAME, EVENT_IDS.RESTART_GAME, EVENT_IDS.PLAYER_JOINED, diff --git a/server/modules/Events.js b/server/modules/Events.js index b2369a7..1f4ab19 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -1,6 +1,6 @@ const GameStateCurator = require('./GameStateCurator'); const GameCreationRequest = require('../model/GameCreationRequest'); -const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS } = require('../config/globals'); +const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS, PRIMITIVES } = require('../config/globals'); const Events = [ { @@ -55,6 +55,42 @@ const Events = [ ); } }, + { + id: EVENT_IDS.CHANGE_NAME, + stateChange: async (game, socketArgs, vars) => { + const toChangeIndex = game.people.findIndex( + (person) => person.id === socketArgs.personId + ); + if (toChangeIndex >= 0) { + if (vars.gameManager.isNameTaken(game, socketArgs.newName)) { + vars.hasNameChanged = false; + if (game.people[toChangeIndex].name.toLowerCase().trim() === socketArgs.newName.toLowerCase().trim()) { + return; + } + vars.ackFn({ errorFlag: 1, message: 'This name is taken.' }); + } else if (socketArgs.newName.length > PRIMITIVES.MAX_PERSON_NAME_LENGTH) { + vars.ackFn({ errorFlag: 1, message: 'Your new name is too long - the max is 30 characters.' }); + vars.hasNameChanged = false; + } else if (socketArgs.newName.length === 0) { + vars.ackFn({ errorFlag: 1, message: 'Your new name cannot be empty.' }); + vars.hasNameChanged = false; + } else { + game.people[toChangeIndex].name = socketArgs.newName; + vars.ackFn({ errorFlag: 0, message: 'Name updated!' }); + vars.hasNameChanged = true; + } + } + }, + communicate: async (game, socketArgs, vars) => { + if (vars.hasNameChanged) { + vars.gameManager.namespace.in(game.accessCode).emit( + EVENT_IDS.CHANGE_NAME, + socketArgs.personId, + socketArgs.newName + ); + } + } + }, { id: EVENT_IDS.UPDATE_GAME_ROLES, stateChange: async (game, socketArgs, vars) => { diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index bed0f40..be7f2fd 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -182,7 +182,7 @@ class GameManager { if (matchingPerson) { return Promise.resolve(matchingPerson.cookie); } - if (isNameTaken(game, name)) { + if (this.isNameTaken(game, name)) { return Promise.reject({ status: 400, reason: 'This name is taken.' }); } if (joinAsSpectator @@ -334,6 +334,11 @@ class GameManager { findPersonByField = (game, fieldName, value) => { return game.people.find(person => person[fieldName] === value); } + + isNameTaken (game, name) { + const processedName = name.toLowerCase().trim(); + return game.people.find((person) => person.name.toLowerCase().trim() === processedName); + } } function getRandomInt (max) { @@ -383,11 +388,6 @@ function createRandomId () { return id; } -function isNameTaken (game, name) { - const processedName = name.toLowerCase().trim(); - return game.people.find((person) => person.name.toLowerCase().trim() === processedName); -} - async function addSpectator (game, name, logger, namespace, eventManager, instanceId, refreshGame) { const spectator = new Person( createRandomId(), diff --git a/spec/unit/server/modules/singletons/GameManager_Spec.js b/spec/unit/server/modules/singletons/GameManager_Spec.js index 6afa3ac..72a078a 100644 --- a/spec/unit/server/modules/singletons/GameManager_Spec.js +++ b/spec/unit/server/modules/singletons/GameManager_Spec.js @@ -18,7 +18,7 @@ describe('GameManager', () => { const inObj = { emit: () => {} }; namespace = { in: () => { return inObj; }, to: () => { return inObj; } }; socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } }; - gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, 'test'); + gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENTS.PRODUCTION, 'test'); timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test'); eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test'); eventManager.publisher = { publish: async (...a) => {} };