From 02fd2665f2db6dc968234c22708123cb19b3d790 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 14:12:44 +0200 Subject: [PATCH] Implemented graphql in front and backend - implementation of graphql api-point for bingo - added player view - added player view toggle button - added submit on enter - improved mobile layout --- graphql/bingo.graphql | 43 ++++- public/javascripts/bingo-web.js | 235 ++++++++++++++++++----- public/javascripts/common.js | 33 +++- public/stylesheets/sass/bingo/style.sass | 32 ++- public/stylesheets/sass/classes.sass | 4 +- routes/bingo.js | 108 +++-------- views/bingo/bingo-game.pug | 24 ++- views/bingo/bingo-submit.pug | 2 +- 8 files changed, 336 insertions(+), 145 deletions(-) diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 7270897..82db62c 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -1,22 +1,22 @@ type BingoMutation { # creates a game of bingo and returns the game id - createGame(words: [String!]!, size: Int = 3): BingoGame + createGame(input: CreateGameInput!): BingoGame # submit a bingo to the active game session - submitBingo: Boolean + submitBingo: BingoGame # toggle a word (heared or not) on the sessions grid - toggleWord(word: String, base64Word: String): BingoGrid + toggleWord(input: WordInput!): BingoGrid # set the username of the current session - setUsername(username: String!): BingoUser + setUsername(input: UsernameInput): BingoUser } type BingoQuery { # Returns the currently active bingo game - gameInfo(id: ID): BingoGame + gameInfo(input: IdInput): BingoGame # If there is a bingo in the fields. checkBingo: Boolean @@ -25,6 +25,36 @@ type BingoQuery { activeGrid: BingoGrid } +input CreateGameInput { + + # the words used to fill the bingo grid + words: [String!]! + + # the size of the bingo grid + size: Int! = 3 +} + +input WordInput { + + # the normal word string + word: String + + # the base64-encoded word + base64Word: String +} + +input UsernameInput { + + # the username string + username: String! +} + +input IdInput { + + # the id + id: ID! +} + type BingoGame { # the id of the bingo game @@ -37,7 +67,7 @@ type BingoGame { gridSize: Int # an array of players active in the bingo game - players(id: ID): [BingoUser] + players(input: IdInput): [BingoUser] # the player-ids that scored a bingo bingos: [String]! @@ -78,5 +108,6 @@ type BingoField { # if the word was already heared submitted: Boolean! + # the base64 encoded word base64Word: String } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index fa95dac..c891bc1 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -1,92 +1,233 @@ +/** + * Returns the value of the url-param 'game' + * @returns {string} + */ +function getGameParam() { + return window.location.href.match(/\?game=(\w+)/)[1]; +} + +/** + * Toggles the visiblity of the player.container + */ +function togglePlayerContainer() { + let playerContainer = document.querySelector('#players-container'); + if (playerContainer.getAttribute('class') === 'hidden') + playerContainer.setAttribute('class', ''); + else + playerContainer.setAttribute('class', 'hidden'); +} + +/** + * Submits the bingo words to create a game + * @returns {Promise} + */ async function submitBingoWords() { let textContent = document.querySelector('#bingo-textarea').value; let words = textContent.replace(/[<>]/g, '').split('\n'); let size = document.querySelector('#bingo-grid-size').value; - let dimY = document.querySelector('#bingo-grid-y').value; - let response = await postLocData({ - bingoWords: words, - size: size - }); - let data = JSON.parse(response.data); - let gameid = data.id; - insertParam('game', gameid); + let response = await postGraphqlQuery(` + mutation($words:[String!]!, $size:Int!) { + bingo { + createGame(input: { + words: $words, + size: $size + }) { + id + } + } + }`, { + words: words, + size: Number(size) + }, `/graphql?game=${getGameParam()}`); + if (response.status === 200) { + let gameid = response.data.bingo.createGame.id; + insertParam('game', gameid); + } else { + console.error(response) + } } +/** + * Submits the value of the username-input to set the username. + * @returns {Promise} + */ async function submitUsername() { let unameInput = document.querySelector('#username-input'); let username = unameInput.value; - let response = await postLocData({ + let response = await postGraphqlQuery(` + mutation($username:String!) { + bingo { + setUsername(input: {username: $username}) { + id + username + } + } + }`, { username: username - }); - unameInput.value = ''; - unameInput.placeholder = username; - document.querySelector('#username-form').remove(); - document.querySelector('.greyover').remove(); - - console.log(response); + },`/graphql?game=${getGameParam()}`); + if (response.status === 200) { + unameInput.value = ''; + unameInput.placeholder = response.data.username; + document.querySelector('#username-form').remove(); + document.querySelector('.greyover').remove(); + } else { + console.error(response); + } } +/** + * toggles a word (toggle occures on response) + * @param word {String} - the base64 encoded bingo word + * @returns {Promise} + */ async function submitWord(word) { - let response = await postLocData({ - bingoWord: word - }); - console.log(response); + let response = await postGraphqlQuery(` + mutation($word:String!) { + bingo { + toggleWord(input: {base64Word: $word}) { + bingo + fieldGrid { + submitted + base64Word + } + } + } + }`, { + word: word + },`/graphql?game=${getGameParam()}`); - let data = JSON.parse(response.data); - for (let row of data.fieldGrid) { - for (let field of row) { - document.querySelectorAll(`.bingo-word-panel[b-word="${field.base64Word}"]`).forEach(x => { - x.setAttribute('b-sub', field.submitted); - }); + if (response.status === 200 && response.data.bingo.toggleWord) { + let fieldGrid = response.data.bingo.toggleWord.fieldGrid; + for (let row of fieldGrid) { + for (let field of row) { + document.querySelectorAll(`.bingo-word-panel[b-word="${field.base64Word}"]`).forEach(x => { + x.setAttribute('b-sub', field.submitted); + }); + } + } + if (response.data.bingo.toggleWord.bingo) { + document.querySelector('#bingo-button').setAttribute('class', ''); + } else { + document.querySelector('#bingo-button').setAttribute('class', 'hidden'); } - } - if (data.bingo) { - document.querySelector('#bingo-button').setAttribute('class', ''); } else { - document.querySelector('#bingo-button').setAttribute('class', 'hidden'); + console.error(response); } } +/** + * Submits a bingo (Bingo button is pressed). + * The game is won if the backend validated it. + * @returns {Promise} + */ async function submitBingo() { - let response = await postLocData({ - bingo: true - }); - let data = JSON.parse(response.data); - if (data.bingos.length > 0) { - displayWinner(data.users[data.bingos[0]].username); - clearInterval(refrInterval) + let response = await postGraphqlQuery(` + mutation { + bingo { + submitBingo { + id + bingos + players { + id + username + } + } + } + }`,null,`/graphql?game=${getGameParam()}`); + if (response.status === 200 && response.data.bingo.submitBingo) { + let bingoSession = response.data.bingo.submitBingo; + if (bingoSession.bingos.length > 0) { + displayWinner(bingoSession.players.find(x => x.id === bingoSession.bingos[0]).username); + clearInterval(refrInterval) + } + } else { + console.error(response); } - console.log(response); } +/** + * Refreshes the information (by requesting information about the current game). + * Is used to see if one player has scored a bingo and which players are in the game. + * @returns {Promise} + */ async function refresh() { - let response = await postLocData({}); - if (response.status === 400) - clearInterval(refrInterval); - let data = JSON.parse(response.data); - if (data.bingos.length > 0) { - displayWinner(data.users[data.bingos[0]].username); - clearInterval(refrInterval) + let response = await postGraphqlQuery(` + query { + bingo { + gameInfo { + id + bingos + players { + username + id + } + } + } + }`, null, `/graphql?game=${getGameParam()}`); + if (response.status === 200 && response.data.bingo.gameInfo) { + let bingoSession = response.data.bingo.gameInfo; + + if (bingoSession.bingos.length > 0) { + displayWinner(bingoSession.players.find(x => x.id === bingoSession.bingos[0]).username); + clearInterval(refrInterval) + } else { + for (let player of bingoSession.players) { + let foundPlayerDiv = document.querySelector(`.player-container[b-pid='${player.id}'`); + if (!foundPlayerDiv) { + let playerDiv = document.createElement('div'); + playerDiv.setAttribute('class', 'player-container'); + playerDiv.setAttribute('b-pid', player.id); + playerDiv.innerHTML = `${player.username}`; + document.querySelector('#players-container').appendChild(playerDiv); + } else { + let playerNameSpan = foundPlayerDiv.querySelector('.player-name-span'); + if (playerNameSpan.innerText !== player.username) { + playerNameSpan.innerText = player.username; + } + } + } + } + } else { + if (response.status === 400) + clearInterval(refrInterval); + console.error(response); } - console.log(response); } +/** + * Displays the winner of the game in a popup. + * @param name {String} - the name of the winner + */ function displayWinner(name) { let winnerDiv = document.createElement('div'); let greyoverDiv = document.createElement('div'); let winnerSpan = document.createElement('span'); winnerDiv.setAttribute('class', 'popup'); + winnerDiv.setAttribute('style', 'cursor: pointer'); greyoverDiv.setAttribute('class', 'greyover'); winnerSpan.innerText = `${name} has won!`; winnerDiv.appendChild(winnerSpan); + winnerDiv.onclick = () => { + window.location.reload(); + }; document.body.append(greyoverDiv); document.body.appendChild(winnerDiv); } +/** + * Executes the provided function if the key-event is an ENTER-key + * @param event {Event} - the generated key event + * @param func {function} - the function to execute on enter + */ +function submitOnEnter(event, func) { + if (event.which === 13) + func(); +} + window.onload = () => { if (window && !document.querySelector('#bingoform')) { - refrInterval = setInterval(refresh, 1000); + refrInterval = setInterval(refresh, 1000); // global variable to clear } let gridSizeElem = document.querySelector('#bingo-grid-size'); document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index 0883713..15840a7 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -1,4 +1,4 @@ -function postLocData(postBody) { +function postData(url, postBody) { let request = new XMLHttpRequest(); return new Promise((res, rej) => { @@ -13,12 +13,39 @@ function postLocData(postBody) { rej(request.error); }; - request.open('POST', '#', true); + request.open('POST', url, true); request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); request.send(JSON.stringify(postBody)); }); } +async function postLocData(postBody) { + return await postData('#', postBody); +} + +async function postGraphqlQuery(query, variables, url) { + let body = { + query: query, + variables: variables + }; + let response = await postData(url || '/graphql', body); + let resData = JSON.parse(response.data); + + if (response.status === 200) { + return { + status: response.status, + data: resData.data, + }; + } else { + return { + status: response.status, + data: resData.data, + errors: resData.errors, + requestBody: body + }; + } +} + function insertParam(key, value) { key = encodeURI(key); value = encodeURI(value); @@ -42,4 +69,4 @@ function insertParam(key, value) { } document.location.search = kvp.join('&'); -} \ No newline at end of file +} diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 50631ed..7c745f6 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -18,6 +18,14 @@ textarea #words-container width: 100% height: 80% + #content-container #row-1 #players-container div + display: none + padding: 0 + #username-form + width: calc(100% - 2rem) !important + left: 0 !important + #hide-player-container-button + display: none @media(min-device-width: 641px) textarea @@ -25,7 +33,7 @@ textarea width: 50% #words-container width: 100% - height: 88% + height: 100% .number-input width: 4rem @@ -96,14 +104,36 @@ textarea button cursor: pointer + width: 100% + margin: 1rem auto input[type='text'] cursor: text + width: 100% #username-form * display: inline-block vertical-align: middle +#content-container + display: table + height: 100% + width: 100% + + #row-1 + display: table-row + height: 85% + + #players-container + display: table-cell + padding: 0.5rem + transition-duration: 1s + + .player-container + @include default-element + padding: 0.5rem + max-width: 14rem + .popup @include default-element height: 5% diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 0a6474d..4dffa00 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -2,7 +2,7 @@ display: table-row .hidden - display: None + display: None !important .popup height: 60% @@ -10,4 +10,4 @@ z-index: 1000 position: fixed top: 20% - left: 30% \ No newline at end of file + left: 30% diff --git a/routes/bingo.js b/routes/bingo.js index c370e5e..4b394ac 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -37,8 +37,9 @@ class BingoSession { * @returns {any[]|*} */ players(args) { - if (args.id) - return [this.users[args.id]]; + let input = args? args.input : null; + if (input && input.id) + return [this.users[input.id]]; else return Object.values(this.users); } @@ -224,7 +225,7 @@ router.use((req, res, next) => { router.get('/', (req, res) => { let bingoUser = req.session.bingoUser; if (req.query.game) { - let gameId = req.query.game; + let gameId = req.query.game || bingoUser.game; if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { bingoUser.game = gameId; @@ -234,7 +235,11 @@ router.get('/', (req, res) => { if (!bingoUser.grids[gameId]) { bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words); } - res.render('bingo/bingo-game', {grid: bingoUser.grids[gameId].fieldGrid, username: bingoUser.username}); + res.render('bingo/bingo-game', { + grid: bingoUser.grids[gameId].fieldGrid, + username: bingoUser.username, + players: bingoSession.players() + }); } else { res.render('bingo/bingo-submit'); } @@ -243,81 +248,28 @@ router.get('/', (req, res) => { } }); -router.post('/', (req, res) => { - let data = req.body; - let gameId = req.query.game; - let bingoUser = req.session.bingoUser; - let bingoSession = bingoSessions[gameId]; - - if (data.bingoWords) { - let words = data.bingoWords; - let size = data.size; - let game = new BingoSession(words, size); - - bingoSessions[game.id] = game; - - setTimeout(() => { // delete the game after one day - delete bingoSessions[game.id]; - }, 86400000); - - res.send(game); - } else if (data.username) { - bingoUser.username = data.username; - bingoSessions[gameId].addUser(bingoUser); - - res.send(bingoUser); - } else if (data.game) { - res.send(bingoSessions[data.game]); - } else if (data.bingoWord) { - console.log(typeof bingoUser.grids[gameId]); - if (bingoUser.grids[gameId]) - toggleHeared(data.bingoWord, bingoUser.grids[gameId]); - res.send(bingoUser.grids[gameId]); - } else if (data.bingo) { - if (checkBingo(bingoUser.grids[gameId])) { - if (!bingoSession.bingos.includes(bingoUser.id)) - bingoSession.bingos.push(bingoUser.id); - bingoSession.finished = true; - setTimeout(() => { // delete the finished game after five minutes - delete bingoSessions[gameId]; - }, 360000); - res.send(bingoSession); - } else { - res.status(400); - res.send({'error': "this is not a bingo!"}) - } - } else if (bingoSession) { - res.send(bingoSession); - } else { - res.status(400); - res.send({ - error: 'invalid request data' - }) - } -}); - router.graphqlResolver = (req) => { let bingoUser = req.session.bingoUser || new BingoUser(); let gameId = req.query.game || bingoUser.game || null; let bingoSession = bingoSessions[gameId]; return { // queries - gameInfo: (args) => { - if (args.id) - return bingoSessions[args.id]; + gameInfo: ({input}) => { + if (input && input.id) + return bingoSessions[input.id]; else return bingoSession; }, - checkBingo: (args) => { + checkBingo: () => { return checkBingo(bingoUser.grids[gameId]) }, - activeGrid: (args) => { + activeGrid: () => { return bingoUser.grids[gameId]; }, // mutation - createGame: (args) => { - let words = args.words; - let size = args.size; + createGame: ({input}) => { + let words = input.words; + let size = input.size; let game = new BingoSession(words, size); bingoSessions[game.id] = game; @@ -328,31 +280,33 @@ router.graphqlResolver = (req) => { return game; }, - submitBingo: (args) => { + submitBingo: () => { if (checkBingo(bingoUser.grids[gameId])) { if (!bingoSession.bingos.includes(bingoUser.id)) bingoSession.bingos.push(bingoUser.id); bingoSession.finished = true; setTimeout(() => { // delete the finished game after five minutes delete bingoSessions[gameId]; - }, 360000); - return true; + }, 300000); + return bingoSession; } else { - return false; + return bingoSession; } }, - toggleWord: (args) => { - if (args.word || args.base64Word) { - args.base64Word = args.base64Word || Buffer.from(args.word).toString('base-64'); + toggleWord: ({input}) => { + if (input.word || input.base64Word) { + input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); if (bingoUser.grids[gameId]) - toggleHeared(args.base64Word, bingoUser.grids[gameId]); + toggleHeared(input.base64Word, bingoUser.grids[gameId]); return bingoUser.grids[gameId]; } }, - setUsername: (args) => { - if (args.username) { - bingoUser.username = args.username; - bingoSession.addUser(bingoUser); + setUsername: ({input}) => { + if (input.username) { + bingoUser.username = input.username; + + if (bingoSession) + bingoSession.addUser(bingoUser); return bingoUser; } diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index da29761..a0d3bdf 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -3,13 +3,21 @@ include bingo-layout block content if username === 'anonymous' div(class='greyover') - div(id='username-form') + div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)') input(type='text', id='username-input', placeholder=username) button(onclick='submitUsername()') Set Username - div(id='words-container') - each val in grid - div(class='bingo-word-row') - each field in val - div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub='false') - span= field.word - button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! + div(id='content-container') + button(id='hide-player-container-button', onclick='togglePlayerContainer()') Toggle Player View + div(id='row-1') + div(id='players-container') + each player in players + div(class='player-container', b-pid=`${player.id}`) + span(class='player-name-span')= player.username + div(id='words-container') + each val in grid + div(class='bingo-word-row') + each field in val + div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub=`${field.submitted}`) + span= field.word + div(id='button-container') + button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug index 9a60b0a..2a5228d 100644 --- a/views/bingo/bingo-submit.pug +++ b/views/bingo/bingo-submit.pug @@ -4,7 +4,7 @@ block content div(id='bingoform') div(id='bingoheader') div - input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8) + input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8, onkeypress='submitOnEnter(event, submitBingoWords)') span x span(id='bingo-grid-y', class='number-input') 3 div(class='stretchDiv')