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 0000000..1fc95c2 Binary files /dev/null and b/public/favicon.ico differ 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') 👑