From d566505c3fd0554c74a90cc05e841e04104b5479 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 16 May 2019 11:19:29 +0200 Subject: [PATCH] Bingo lobby improvements - bingo added kicking of player - bingo added display of admin - bingo added grid size input --- CHANGELOG.md | 6 +- graphql/bingo.graphql | 2 +- misc/usernames.txt | 2 + public/favicon.ico | Bin 0 -> 102004 bytes public/javascripts/bingo-web.js | 80 ++++++++++++++++++----- public/stylesheets/sass/bingo/style.sass | 15 ++++- public/stylesheets/sass/style.sass | 13 +++- routes/bingo.js | 60 +++++++++++++---- views/bingo/bingo-lobby.pug | 2 + views/bingo/bingo-players.pug | 4 ++ 10 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 public/favicon.ico diff --git a/CHANGELOG.md b/CHANGELOG.md index f76a312..ff336d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - css for startpage (wip) - file for css animations - pug file for startpage +- bingo lobbys +- kick function for bingo +- grid size input ## Changed - changed export of `app.js` to the asynchronous init function that returns the app object - `bin/www` now calls the init function of `app.js` -- graphql api +- graphql bingo api +- bingo frontend ### Removed diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 9463835..9f077a2 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -19,7 +19,7 @@ type LobbyMutation { leave: Boolean "kicks a player from the lobby" - kickPlayer(playerId: ID!): BingoLobby + kickPlayer(pid: ID!): BingoPlayer "starts a round in a lobby if the user is the admin" startRound: BingoRound diff --git a/misc/usernames.txt b/misc/usernames.txt index 0ba10d3..66096b4 100644 --- a/misc/usernames.txt +++ b/misc/usernames.txt @@ -9,3 +9,5 @@ Angry Koala Dragonslayer Goblin Slayer useless Aqua +theP0wner79 +Pr0wn diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1fc95c2a45174125115da04e5e70456a271a395f GIT binary patch literal 102004 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 AmH+?% literal 0 HcmV?d00001 diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 5fdbc37..cc3fff3 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -101,6 +101,32 @@ async function leaveLobby() { } } +/** + * Kicks a player by id. + * @param playerId + * @returns {Promise} + */ +async function kickPlayer(pid) { + let response = await postGraphqlQuery(` + mutation ($lobbyId: ID!, $playerId:ID!) { + bingo { + mutateLobby(id: $lobbyId) { + kickPlayer(pid: $playerId) { + id + } + } + } + } + `, {lobbyId: getLobbyParam(), playerId: pid}); + if (response.status === 200) { + let kickId = response.data.bingo.mutateLobby.kickPlayer.id; + document.querySelector(`.playerEntryContainer[b-pid='${kickId}'`).remove(); + } else { + showError('Failed to kick player!'); + console.error(response); + } +} + /** * Sends a message to the chat * @returns {Promise} @@ -138,21 +164,27 @@ async function sendChatMessage() { /** * Sets the words for the lobby * @param words + * @param gridSize * @returns {Promise} */ -async function setLobbyWords(words) { +async function setLobbySettings(words, gridSize) { + gridSize = Number(gridSize); let response = await postGraphqlQuery(` - mutation($lobbyId:ID!, $words:[String!]!){ + mutation ($lobbyId: ID!, $words: [String!]!, $gridSize:Int!) { bingo { - mutateLobby(id:$lobbyId) { - setWords(words:$words) { + mutateLobby(id: $lobbyId) { + setWords(words: $words) { words { content } } + setGridSize(gridSize: $gridSize) { + gridSize + } } } - }`, {lobbyId: getLobbyParam(), words: words}); + } + `, {lobbyId: getLobbyParam(), words: words, gridSize: gridSize}); if (response.status === 200) { return response.data.bingo.mutateLobby.setWords.words; } else { @@ -168,7 +200,8 @@ async function setLobbyWords(words) { async function startRound() { let textinput = document.querySelector('#input-bingo-words'); let words = getLobbyWords(); - let resultWords = await setLobbyWords(words); + let gridSize = document.querySelector('#input-grid-size').value || 3; + let resultWords = await setLobbySettings(words, gridSize); textinput.value = resultWords.map(x => x.content).join('\n'); let response = await postGraphqlQuery(` mutation($lobbyId:ID!){ @@ -351,11 +384,17 @@ function addChatMessage(messageObject) { * Adds a player to the player view * @param player */ -function addPlayer(player) { +function addPlayer(player, options) { let playerContainer = document.createElement('div'); playerContainer.setAttribute('class', 'playerEntryContainer'); playerContainer.setAttribute('b-pid', player.id); - playerContainer.innerHTML = `${player.username}`; + + if (options.isAdmin && player.id !== options.admin) + playerContainer.innerHTML = ``; + playerContainer.innerHTML += `${player.username}`; + + if (player.id === options.admin) + playerContainer.innerHTML += " 👑"; document.querySelector('#player-list').appendChild(playerContainer); } @@ -403,22 +442,31 @@ async function refreshChat() { async function refreshPlayers() { try { let response = await postGraphqlQuery(` - query($lobbyId:ID!){ + query ($lobbyId: ID!) { bingo { - lobby(id:$lobbyId) { + player { + id + } + lobby(id: $lobbyId) { players { id username - wins(lobbyId:$lobbyId) + wins(lobbyId: $lobbyId) + } + admin { + id } } } - }`, {lobbyId: getLobbyParam()}); + } + `, {lobbyId: getLobbyParam()}); if (response.status === 200) { let players = response.data.bingo.lobby.players; + let adminId = response.data.bingo.lobby.admin.id; + let isAdmin = response.data.bingo.player.id === adminId; for (let player of players) if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) - addPlayer(player); + addPlayer(player, {admin: adminId, isAdmin: isAdmin}); } else { showError('Failed to refresh players'); console.error(response); @@ -572,7 +620,9 @@ window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { window.addEventListener("keydown", async (e) => { if (e.which === 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) { e.preventDefault(); - if (document.querySelector('#input-bingo-words')) - await setLobbyWords(getLobbyWords()); + if (document.querySelector('#input-bingo-words')) { + let gridSize = document.querySelector('#input-grid-size').value || 3; + await setLobbySettings(getLobbyWords(), gridSize); + } } }, false); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 3059e50..a8ba59e 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -82,9 +82,13 @@ #input-bingo-words width: 100% - height: calc(100% - 7rem) + height: calc(100% - 10rem) margin: 0 + #input-grid-size + height: 3rem + width: 4rem + #button-round-start, #button-leave height: 3rem width: 100% @@ -116,6 +120,13 @@ height: calc(100% - 6rem) overflow-y: auto + .kickPlayerButton + background-color: #0000 + border: none + padding: 0 + margin: 0 0.5rem 0 0 + font-size: 1em + #container-grid display: table height: calc(100% - 2em) @@ -183,7 +194,7 @@ #container-bingo-lobby display: grid - grid-template: 5% 5% 85% 5% / 5% 30% 30% 30% 5% + grid-template: 0 10% 85% 5% / 5% 30% 30% 30% 5% height: 100% width: 100% diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 2628fa9..ec096b1 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -46,11 +46,20 @@ button:active background-color: lighten($secondary, 15%) input - @include default-element + background-color: lighten($primary, 10%) + color: $primarySurface + border: 1px solid $inactive + transition-duration: 0.2s font-size: 1.2rem - background-color: lighten($primary, 15%) padding: 0.7rem +input:focus + background-color: lighten($primary, 15%) + border: 1px solid $primarySurface + +input[type='number'] + text-align: center + textarea background-color: lighten($primary, 15%) color: $primarySurface diff --git a/routes/bingo.js b/routes/bingo.js index 127c4f6..1bf6421 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -854,6 +854,15 @@ class LobbyWrapper { } } + /** + * Returns if the lobby exists (based on one loaded attribute) + * @returns {Promise} + */ + async exists() { + await this._loadLobbyInfo(); + return !!this.expire; + } + /** * returns the players in the lobby * @returns {Promise} @@ -1025,6 +1034,18 @@ class LobbyWrapper { await this._loadLobbyInfo(true); } + /** + * Removes a player from the lobby + * @param playerId + * @returns {Promise} + */ + async removePlayer(playerId) { + await bdm.removePlayerFromLobby(playerId, this.id); + let username = await new PlayerWrapper(playerId).username(); + await bdm.addInfoMessage(this.id, `${username} left.`); + await this._loadLobbyInfo(true); + } + /** * Returns if the lobby is in an active round * @returns {Promise} @@ -1236,9 +1257,9 @@ router.get('/', async (req, res) => { let playerId = req.session.bingoPlayerId; if (!playerId) req.session.bingoPlayerId = playerId = (await bdm.addPlayer(shuffleArray(playerNames)[0])).id; - if (req.query.g) { + let lobbyWrapper = new LobbyWrapper(req.query.g); + if (req.query.g && await lobbyWrapper.exists()) { let lobbyId = req.query.g; - let lobbyWrapper = new LobbyWrapper(lobbyId); if (!(await lobbyWrapper.roundActive())) { if (!await lobbyWrapper.hasPlayer(playerId)) @@ -1249,17 +1270,34 @@ router.get('/', async (req, res) => { res.render('bingo/bingo-lobby', { players: playerData, isAdmin: (playerId === admin.id), + adminId: admin.id, words: words, - wordString: words.join('\n')}); + wordString: words.join('\n'), + gridSize: await lobbyWrapper.gridSize() + }); } else { if (await lobbyWrapper.hasPlayer(playerId)) { let playerData = await getPlayerData(lobbyWrapper); let grid = await getGridData(lobbyId, playerId); - res.render('bingo/bingo-round', {players: playerData, grid: grid}); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-round', { + players: playerData, + grid: grid, + isAdmin: (playerId === admin.id), + adminId: admin.id + }); } else { let playerData = await getPlayerData(lobbyWrapper); let admin = await lobbyWrapper.admin(); - res.render('bingo/bingo-lobby', {players: playerData, isAdmin: (playerId === admin.id)}); + let words = await getWordsData(lobbyWrapper); + res.render('bingo/bingo-lobby', { + players: playerData, + isAdmin: (playerId === admin.id), + adminId: admin.id, + words: words, + wordString: words.join('\n'), + gridSize: await lobbyWrapper.gridSize() + }); } } } else { @@ -1314,15 +1352,15 @@ router.graphqlResolver = async (req, res) => { return { join: async () => { if (playerId) { - let result = await bdm.addPlayerToLobby(playerId, lobbyId); - return new LobbyWrapper(result.lobby_id); + await lobbyWrapper.addPlayer(playerId); + return lobbyWrapper; } else { res.status(400); } }, leave: async () => { if (playerId) { - await bdm.removePlayerFromLobby(playerId, lobbyId); + await lobbyWrapper.removePlayer(playerId); return true; } else { res.status(400); @@ -1331,8 +1369,8 @@ router.graphqlResolver = async (req, res) => { kickPlayer: async ({pid}) => { let admin = await lobbyWrapper.admin(); if (admin.id === playerId) { - let result = await bdm.removePlayerFromLobby(pid, lobbyId); - return new LobbyWrapper(result.id, result); + await lobbyWrapper.removePlayer(pid); + return new PlayerWrapper(pid); } }, startRound: async () => { @@ -1369,7 +1407,7 @@ router.graphqlResolver = async (req, res) => { submitBingo: async () => { let isBingo = await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})).bingo(); let currentRound = await lobbyWrapper.currentRound(); - if (isBingo) { + if (isBingo && await lobbyWrapper.hasPlayer(playerId)) { let result = await currentRound.setWinner(playerId); if (result) return currentRound; diff --git a/views/bingo/bingo-lobby.pug b/views/bingo/bingo-lobby.pug index 2ae7641..e46351d 100644 --- a/views/bingo/bingo-lobby.pug +++ b/views/bingo/bingo-lobby.pug @@ -7,6 +7,8 @@ block content div(id='container-lobby-settings') h1 Words if isAdmin + span Grid Size: + input(id='input-grid-size' type='number' value=gridSize) textarea(id='input-bingo-words')= wordString button(id='button-round-start' onclick='startRound()') Start Round else diff --git a/views/bingo/bingo-players.pug b/views/bingo/bingo-players.pug index aefa5d3..d17f2de 100644 --- a/views/bingo/bingo-players.pug +++ b/views/bingo/bingo-players.pug @@ -3,4 +3,8 @@ div(id='container-players') div(id='player-list') each player in players div(class='playerEntryContainer', b-pid=`${player.id}`) + if isAdmin && player.id !== adminId + button(class='kickPlayerButton' onclick=`kickPlayer(${player.id})`) ❌ span(class='playerNameSpan')= player.username + if player.id === adminId + span(class='adminSpan') 👑