From 5c10862b196e64bf890e802917ce6f4eec817bf6 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 14 May 2019 21:33:05 +0200 Subject: [PATCH] Added bingo startpage (wip) - added css for startpage (wip) - added file for css animations - added pug file for bingo starpage --- CHANGELOG.md | 5 + public/javascripts/bingo-web.js | 331 ++--------------------- public/javascripts/common.js | 31 +++ public/stylesheets/sass/animations.sass | 15 + public/stylesheets/sass/bingo/style.sass | 57 ++-- public/stylesheets/sass/classes.sass | 26 ++ public/stylesheets/sass/mixins.sass | 6 + public/stylesheets/sass/vars.sass | 2 + routes/bingo.js | 29 +- views/bingo/bingo-create.pug | 16 ++ 10 files changed, 170 insertions(+), 348 deletions(-) create mode 100644 public/stylesheets/sass/animations.sass create mode 100644 views/bingo/bingo-create.pug diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e50695..f76a312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,15 +20,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - sql scripts for bingo - data management class for bingo - libs with utils and global variables +- css for startpage (wip) +- file for css animations +- pug file for startpage ## 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 ### Removed - sqlite3 sesssion storage +- old frontend ### Fixed diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 0b953c6..7babd77 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -5,272 +5,41 @@ */ function getGameParam() { let matches = window.location.href.match(/\?game=(\w+)/); - if (matches) + if (matches) { return matches[1]; - else + } else { return ''; + } } /** - * Submits the bingo words to create a game - * @returns {Promise} + * Submits the value of the username-input to set the username. + * @returns {Promise} */ -async function submitBingoWords() { - let textContent = document.querySelector('#bingo-textarea').value; - let words = textContent.replace(/[<>]/g, '').split('\n').filter((el) => { - return (!!el && el.length > 0); // remove empty strings and non-types from word array - }); - if (words.length === 0) { - showError('You need to provide at least one word!'); - } else { - let size = document.querySelector('#bingo-grid-size').value; +async function submitUsername() { + let unameInput = document.querySelector('#input-username'); + let username = unameInput.value.replace(/^\s+|\s+$/g, ''); + if (username.length > 1) { let response = await postGraphqlQuery(` - mutation($words:[String!]!, $size:Int!) { + mutation($username:String!) { bingo { - createGame(input: { - words: $words, - size: $size - }) { + setUsername(username: $username) { id + username } } - }`, { - words: words, - size: Number(size) - }, `/graphql?game=${getGameParam()}`); + }`, {username: username}); if (response.status === 200) { - let gameid = response.data.bingo.createGame.id; - insertParam('game', gameid); - } else { - showError(`Failed to create game. HTTP Error: ${response.status}`); - console.error(response); - } - } -} - -/** - * Gets the followup bingoSession and redirects to it - * @returns {Promise} - */ -async function createFollowup() { - let response = await postGraphqlQuery(` - mutation { - bingo { - createFollowupGame { - id - } - } - }`, null, `/graphql?game=${getGameParam()}`); - if (response.status === 200 && response.data.bingo.createFollowupGame) { - let gameid = response.data.bingo.createFollowupGame.id; - insertParam('game', gameid); - } else { - showError(`Failed to create follow up game. HTTP Error: ${response.status}`); - 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.replace(/^\s+|\s+$/g, ''); - if (username.length > 1 && username !== 'anonymous') { - let response = await postGraphqlQuery(` - mutation($username:String!) { - bingo { - setUsername(input: {username: $username}) { - id - username - } - } - }`, { - username: username - }, `/graphql?game=${getGameParam()}`); - if (response.status === 200) { - unameInput.value = ''; - unameInput.placeholder = response.data.username; - document.querySelector('#username-form').remove(); - document.querySelector('.greyover').remove(); + return true; } else { showError(`Failed to submit username. HTTP Error: ${response.status}`); console.error(response); + return false; } } else { showError('You need to provide a username (minimum 2 characters)!'); - } -} - -/** - * toggles a word (toggle occures on response) - * @param word {String} - the base64 encoded bingo word - * @returns {Promise} - */ -async function submitWord(word) { - let response = await postGraphqlQuery(` - mutation($word:String!) { - bingo { - toggleWord(input: {base64Word: $word}) { - bingo - fieldGrid { - submitted - base64Word - } - } - } - }`, { - word: word - }, `/graphql?game=${getGameParam()}`); - - 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'); - - } else { - showError(`Failed to submit word. HTTP Error: ${response.status}`); - console.error(response); - } -} - -/** - * Refreshes the bingo grid. Shows the bingo button if a bingo is possible - * @returns {Promise} - */ -async function refreshBingoGrid() { - let response = await postGraphqlQuery(` - query { - bingo { - activeGrid { - bingo - fieldGrid { - word - base64Word - submitted - } - } - } - }`, {}, `/graphql?game=${getGameParam()}`); - - if (response.status === 200 && response.data.bingo.activeGrid) { - let fieldGrid = response.data.bingo.activeGrid.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.activeGrid.bingo) - document.querySelector('#bingo-button').setAttribute('class', ''); - else - document.querySelector('#bingo-button').setAttribute('class', 'hidden'); - - } else { - showError(`Failed to submit word. HTTP Error: ${response.status}`); - 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 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 { - showError(`Failed to submit Bingo. HTTP Error: ${response.status}`); - console.error(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() { - await refreshBingoGrid(); - let response = await postGraphqlQuery(` - query { - bingo { - gameInfo { - id - bingos - players { - username - id - } - getMessages { - id - username - type - htmlContent - } - } - } - }`, 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; - - } - } - } - for (let chatMessage of bingoSession.getMessages) - if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`)) - addChatMessage(chatMessage); - - } else { - if (response.status === 400) - clearInterval(refrInterval); - console.error(response); - showError('No session found. Are cookies allowed?'); + return false; } } @@ -304,40 +73,16 @@ function showError(errorMessage) { errorDiv.setAttribute('class', 'errorDiv'); errorDiv.innerHTML = `${errorMessage}`; let contCont = document.querySelector('#content-container'); - if (contCont) + if (contCont) { contCont.appendChild(errorDiv); - else + } else { alert(errorMessage); + } setTimeout(() => { errorDiv.remove(); }, 10000); } -async function sendChatMessage() { - let messageInput = document.querySelector('#chat-input'); - if (messageInput.value && messageInput.value.length > 0) { - let message = messageInput.value; - let response = await postGraphqlQuery(` - mutation($message: String!) { - bingo { - sendChatMessage(input: { message: $message }) { - id - htmlContent - username - type - } - } - }`, {message: message}, `/graphql?game=${getGameParam()}`); - if (response.status === 200) { - addChatMessage(response.data.bingo.sendChatMessage); - messageInput.value = ''; - } else { - console.error(response); - showError('Error when sending message.'); - } - } -} - /** * Adds a message to the chat * @param messageObject {Object} - the message object returned by graphql @@ -346,55 +91,25 @@ function addChatMessage(messageObject) { let msgSpan = document.createElement('span'); msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('msg-id', messageObject.id); - if (messageObject.type === "USER") + if (messageObject.type === "USER") { msgSpan.innerHTML = ` ${messageObject.username}: ${messageObject.htmlContent}`; - else + } else { msgSpan.innerHTML = ` ${messageObject.htmlContent}`; + } let chatContent = document.querySelector('#chat-content'); chatContent.appendChild(msgSpan); chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom } -/** - * 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(); -} - -/** - * Toggles the displayChat class on the content container to switch between chat-view and grid view - */ -function toggleChatView() { - let contentContainer = document.querySelector('#content-container'); - if (contentContainer.getAttribute('class') === 'displayChat') - contentContainer.setAttribute('class', ''); - else - contentContainer.setAttribute('class', 'displayChat'); -} -window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { +window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { promiseRejectionEvent.promise.catch(err => console.log(err)); showError('Connection problems... Is the server down?'); }); window.onload = () => { - if (document.querySelector('#chat-container')) - refresh(); - if (window && !document.querySelector('#bingoform')) - refrInterval = setInterval(refresh, 1000); // eslint-disable-line no-undef - - let gridSizeElem = document.querySelector('#bingo-grid-size'); - document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; - gridSizeElem.oninput = () => { - document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; - document.querySelector('#word-count').innerText = `Please provide at least ${gridSizeElem.value**2} phrases:`; - }; }; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index b38cacd..91c1c13 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -91,3 +91,34 @@ function insertParam(key, value) { kvp[kvp.length] = [key, value].join('='); document.location.search = kvp.join('&'); } + +/** + * 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(); +} + +/** + * Wrapper around a function to use the status indicator + * @param func {function} - the function to execute + * @param indicatorSelector {String} - a selector for the status indicator + * @returns {Promise} + */ +async function indicateStatus(func, indicatorSelector) { + let statusIndicator = document.querySelector(indicatorSelector); + statusIndicator.setAttribute('status', 'pending'); + try { + let result = await func(); + if (result) + statusIndicator.setAttribute('status', 'success'); + else + statusIndicator.setAttribute('status', 'error'); + } catch (err) { + console.error(err); + statusIndicator.setAttribute('status', 'error'); + } +} diff --git a/public/stylesheets/sass/animations.sass b/public/stylesheets/sass/animations.sass new file mode 100644 index 0000000..69245b0 --- /dev/null +++ b/public/stylesheets/sass/animations.sass @@ -0,0 +1,15 @@ +@keyframes pulse-text + 0% + font-size: 0.8em + 50% + font-size: 1.2em + 100% + font-size: 0.8em + +@keyframes pulse-opacity + 0% + opacity: 0.8 + 50% + opacity: 1 + 100% + opacity: 0.8 diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 5a95460..9596e6a 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -41,10 +41,6 @@ textarea #words-container display: none !important - #username-form - width: calc(100% - 2rem) !important - left: 0 !important - #hide-player-container-button display: none @@ -138,32 +134,6 @@ textarea #bingo-button transition-duration: 0.8s -#username-form - @include default-element - position: fixed - display: block - height: calc(50% - 1rem) - width: calc(40% - 1rem) - top: 25% - left: 30% - text-align: center - vertical-align: middle - padding: 1rem - z-index: 1000 - - 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: grid grid-template-columns: 25% 75% @@ -299,3 +269,30 @@ textarea top: 0 left: 0 background-color: rgba(0, 0, 0, 0.5) + +#container-bingo-create + display: grid + grid-template-columns: 10% 80% 10% + grid-template-rows: 5% 10% 10% 70% 5% + height: 100% + width: 100% + + #username-form + @include gridPosition(2, 3, 2, 3) + margin: auto + + .statusIndicator + height: 1em + width: 1em + display: inline-block + margin: auto 1em + + #input-username + margin: 0 0 0 3em + + #lobby-form + @include gridPosition(3, 4, 2, 3) + margin: auto + + button + width: 100% diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 2b4d91a..e73f69f 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -1,3 +1,6 @@ +@import vars +@import animations + .tableRow display: table-row @@ -17,3 +20,26 @@ .inline-grid display: inline-grid + +.statusIndicator + min-height: 1em + min-width: 1em + border-radius: 1em + transition-duration: 1s + +.statusIndicator:before + content: "" + +.statusIndicator[status='success']:before + content: "✓" + color: $success + +.statusIndicator[status='error']:before + content: "❌" + color: $error + +.statusIndicator[status='pending'] + background-color: $pending + animation-name: pulse-opacity + animation-duration: 5s + animation-iteration-count: infinite diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass index 0b60140..17fdf5d 100644 --- a/public/stylesheets/sass/mixins.sass +++ b/public/stylesheets/sass/mixins.sass @@ -5,3 +5,9 @@ color: $primarySurface border: 2px solid $primarySurface transition-duration: 0.2s + +@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd) + grid-row-start: $rowStart + grid-row-end: $rowEnd + grid-column-start: $columnStart + grid-column-end: $columnEnd diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass index 365b26d..b88bf64 100644 --- a/public/stylesheets/sass/vars.sass +++ b/public/stylesheets/sass/vars.sass @@ -4,3 +4,5 @@ $secondary: teal $borderRadius: 20px $inactive: #aaa $error: #a00 +$success: #0a0 +$pending: #aa0 diff --git a/routes/bingo.js b/routes/bingo.js index 5e6785d..ea1b7f7 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1142,8 +1142,8 @@ router.use(async (req, res, next) => { router.get('/', (req, res) => { let bingoUser = req.session.bingoUser; - if (req.query.game) { - let gameId = req.query.game || bingoUser.game; + if (req.query.g) { + let lobbyId = req.query.g; if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { bingoUser.game = gameId; @@ -1163,7 +1163,7 @@ router.get('/', (req, res) => { res.render('bingo/bingo-submit'); } } else { - res.render('bingo/bingo-submit'); + res.render('bingo/bingo-create'); } }); @@ -1188,7 +1188,7 @@ router.graphqlResolver = async (req, res) => { }, // mutations setUsername: async ({username}) => { - username = username.substring(0, 30); // only allow 30 characters + username = replaceTagSigns(username.substring(0, 30)); // only allow 30 characters if (!playerId) { req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; @@ -1228,16 +1228,25 @@ router.graphqlResolver = async (req, res) => { } }, kickPlayer: async ({pid}) => { - let result = await bdm.removePlayerFromLobby(pid, lobbyId); - return new LobbyWrapper(result.id, result); + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + let result = await bdm.removePlayerFromLobby(pid, lobbyId); + return new LobbyWrapper(result.id, result); + } }, startRound: async () => { - await lobbyWrapper.startNewRound(); - return lobbyWrapper.currentRound(); + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.startNewRound(); + return lobbyWrapper.currentRound(); + } }, setGridSize: async ({gridSize}) => { - await lobbyWrapper.setGridSize(gridSize); - return lobbyWrapper; + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.setGridSize(gridSize); + return lobbyWrapper; + } }, setWords: async({words}) => { let admin = await lobbyWrapper.admin(); diff --git a/views/bingo/bingo-create.pug b/views/bingo/bingo-create.pug new file mode 100644 index 0000000..c767f52 --- /dev/null +++ b/views/bingo/bingo-create.pug @@ -0,0 +1,16 @@ +extends bingo-layout + +block content + div(id='container-bingo-create') + div(id='username-form') + input(id='input-username' + type='text' + placeholder='Enter your name' + onkeydown='submitOnEnter(event, () => indicateStatus(submitUsername, "#username-status"))') + button( + id='submit-username' + onclick='indicateStatus(submitUsername, "#username-status")') Set Username + div(id='username-status' class='statusIndicator') + div(id='lobby-form') + button(id='join-lobby' onclick='joinLobby()') Join Lobby + button(id='create-lobby' onclick='createLobby()') Create Lobby