diff --git a/app.js b/app.js index 6fa7cdd..af39593 100644 --- a/app.js +++ b/app.js @@ -21,10 +21,10 @@ let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); if (fsx.existsSync('config.yaml')) Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); -let graphqlResolver = (request) => { +let graphqlResolver = (request, response) => { return { time: Date.now(), - bingo: bingoRouter.graphqlResolver(request) + bingo: bingoRouter.graphqlResolver(request, response) } }; let app = express(); @@ -58,10 +58,10 @@ app.use('/', indexRouter); app.use('/users', usersRouter); app.use(/\/riddle(\/.*)?/, riddleRouter); app.use('/bingo', bingoRouter); -app.use('/graphql', graphqlHTTP(request => { +app.use('/graphql', graphqlHTTP((request, response) => { return { schema: buildSchema(importSchema('./graphql/schema.graphql')), - rootValue: graphqlResolver(request), + rootValue: graphqlResolver(request, response), context: {session: request.session}, graphiql: true }; diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 82db62c..43e0d79 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -11,6 +11,9 @@ type BingoMutation { # set the username of the current session setUsername(input: UsernameInput): BingoUser + + # recreates the active game to a follow-up + createFollowupGame: BingoGame } type BingoQuery { @@ -74,6 +77,9 @@ type BingoGame { # if the game has already finished finished: Boolean + + # the id of the followup game if it has been created + followup: ID } type BingoUser { diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index c9ee9f6..882af3a 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -10,45 +10,63 @@ function getGameParam() { return ''; } -/** - * 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 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; + + 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 { + 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($words:[String!]!, $size:Int!) { + mutation { bingo { - createGame(input: { - words: $words, - size: $size - }) { + createFollowupGame { id } } - }`, { - words: words, - size: Number(size) - }, `/graphql?game=${getGameParam()}`); - if (response.status === 200) { - let gameid = response.data.bingo.createGame.id; + }`,null,`/graphql?game=${getGameParam()}`); + if (response.status === 200 && response.data.bingo.createFollowupGame) { + let gameid = response.data.bingo.createFollowupGame.id; insertParam('game', gameid); } else { - console.error(response) + showError(`Failed to create follow up game. HTTP Error: ${response.status}`); + console.error(response); } } @@ -76,6 +94,7 @@ async function submitUsername() { document.querySelector('#username-form').remove(); document.querySelector('.greyover').remove(); } else { + showError(`Failed to submit username. HTTP Error: ${response.status}`); console.error(response); } } @@ -116,6 +135,7 @@ async function submitWord(word) { document.querySelector('#bingo-button').setAttribute('class', 'hidden'); } } else { + showError(`Failed to submit word. HTTP Error: ${response.status}`); console.error(response); } } @@ -146,6 +166,7 @@ async function submitBingo() { clearInterval(refrInterval) } } else { + showError(`Failed to submit Bingo. HTTP Error: ${response.status}`); console.error(response); } } @@ -196,6 +217,7 @@ async function refresh() { if (response.status === 400) clearInterval(refrInterval); console.error(response); + showError('No session found. Are cookies allowed?'); } } @@ -206,19 +228,38 @@ async function refresh() { 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'); + winnerDiv.innerHTML = ` +

${name} has won!

+ + + `; greyoverDiv.setAttribute('class', 'greyover'); - winnerSpan.innerText = `${name} has won!`; - winnerDiv.appendChild(winnerSpan); - winnerDiv.onclick = () => { - window.location.reload(); - }; + //winnerDiv.onclick = () => { + // window.location.reload(); + //}; document.body.append(greyoverDiv); document.body.appendChild(winnerDiv); } +/** + * Shows an error Message. + * @param errorMessage + */ +function showError(errorMessage) { + let errorDiv = document.createElement('div'); + errorDiv.setAttribute('class', 'errorDiv'); + errorDiv.innerHTML = `${errorMessage}`; + let contCont = document.querySelector('#content-container'); + if (contCont) + contCont.appendChild(errorDiv); + else + alert(errorMessage); + setTimeout(() => { + errorDiv.remove(); + }, 10000); +} + /** * Executes the provided function if the key-event is an ENTER-key * @param event {Event} - the generated key event @@ -229,6 +270,11 @@ function submitOnEnter(event, func) { func(); } +window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { + promiseRejectionEvent.promise.catch(err => console.log(err)); + showError('Connection problems... Is the server down?'); +}); + window.onload = () => { if (window && !document.querySelector('#bingoform')) { refrInterval = setInterval(refresh, 1000); // global variable to clear @@ -237,5 +283,6 @@ window.onload = () => { 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/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 7c745f6..d79b6c2 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -1,15 +1,16 @@ @import ../mixins @import ../vars -button - margin: 1rem - textarea @include default-element display: block margin: 1rem border-radius: 0 font-size: 0.8em + background-color: lighten($primary, 15%) + +#word-count + margin: 1rem @media(max-device-width: 641px) textarea @@ -18,14 +19,20 @@ textarea #words-container width: 100% height: 80% - #content-container #row-1 #players-container div - display: none - padding: 0 + #content-container + grid-template-columns: 0 100% !important + grid-template-rows: 10% 80% 10% !important + #players-container div + display: none + padding: 0 #username-form width: calc(100% - 2rem) !important left: 0 !important #hide-player-container-button display: none + .popup + width: calc(100% - 2rem) !important + left: 0 !important @media(min-device-width: 641px) textarea @@ -70,11 +77,13 @@ textarea border-collapse: collapse text-align: center vertical-align: middle + user-select: none span vertical-align: middle display: inline-block word-break: break-word + user-select: none .bingo-word-panel:hover background-color: darken($primary, 2%) @@ -116,35 +125,83 @@ textarea vertical-align: middle #content-container - display: table + display: grid + grid-template-columns: 20% 80% + grid-template-rows: 10% 80% 10% height: 100% width: 100% - #row-1 - display: table-row - height: 85% + #button-container + grid-column-start: 1 + grid-column-end: 1 + grid-row-start: 1 + grid-row-end: 1 + display: grid + margin: 1rem + font-size: inherit + + button + font-size: inherit + padding: 0 + + #players-container + padding: 0 0.5rem + transition-duration: 1s + grid-column-start: 1 + grid-column-end: 1 + grid-row-start: 2 + grid-row-end: 2 + + h1 + margin: 0 0 1rem 0 - #players-container + #words-container + grid-column-start: 2 + grid-column-end: 2 + grid-row-start: 2 + grid-row-end: 2 + + .errorDiv + grid-column-start: 2 + grid-column-end: 2 + grid-row-start: 3 + grid-row-end: 3 + background-color: $error + text-align: center + margin: 0.75rem 0 + border-radius: 1rem + height: calc(100% - 1.5rem) + display: table + span display: table-cell - padding: 0.5rem - transition-duration: 1s + font-size: 1.8rem + vertical-align: middle - .player-container - @include default-element - padding: 0.5rem - max-width: 14rem +.player-container + @include default-element + padding: 0.5rem + margin: 0 0 1rem 0 + max-width: 14rem + border-radius: 0 + color: $primarySurface + border: 1px solid $inactive .popup @include default-element - height: 5% - width: 40% - top: 47.5% + position: fixed + display: grid + height: calc(50% - 1rem) + width: calc(40% - 1rem) + top: 25% left: 30% text-align: center - transition-duration: 1s - span - margin: 2% - display: block + vertical-align: middle + padding: 1rem + z-index: 1000 + + button + margin: 1rem + font-size: 2rem .greyover width: 100% @@ -153,4 +210,4 @@ textarea z-index: 99 top: 0 left: 0 - background-color: transparentize($primary, 0.5) + background-color: rgba(0,0,0,0.5) diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 4dffa00..2b4d91a 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -11,3 +11,9 @@ position: fixed top: 20% left: 30% + +.grid + display: grid + +.inline-grid + display: inline-grid diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 8b14e00..066a693 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -36,16 +36,20 @@ button font-size: 1.2rem padding: 0.7rem transition-duration: 0.2s + background-color: $secondary button:hover - background-color: darken($primary, 2%) + background-color: darken($secondary, 2%) cursor: pointer button:active - background-color: lighten($primary, 15%) + background-color: lighten($secondary, 15%) input @include default-element font-size: 1.2rem - background-color: lighten($primary, 10%) + background-color: lighten($primary, 15%) padding: 0.7rem + +textarea + background-color: lighten($primary, 15%) diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass index 9f8b9b2..365b26d 100644 --- a/public/stylesheets/sass/vars.sass +++ b/public/stylesheets/sass/vars.sass @@ -1,4 +1,6 @@ $primary: #223 $primarySurface: white - -$borderRadius: 20px \ No newline at end of file +$secondary: teal +$borderRadius: 20px +$inactive: #aaa +$error: #a00 diff --git a/routes/bingo.js b/routes/bingo.js index 4b394ac..ec1cb0a 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -20,6 +20,7 @@ class BingoSession { this.users = {}; this.bingos = []; // array with the users that already had bingo this.finished = false; + this.followup = null; } /** @@ -43,6 +44,17 @@ class BingoSession { else return Object.values(this.users); } + + /** + * Creates a followup BingoSession + * @returns {BingoSession} + */ + createFollowup() { + let followup = new BingoSession(this.words, this.gridSize); + this.followup = followup.id; + bingoSessions[followup.id] = followup; + return followup; + } } class BingoUser { @@ -109,7 +121,7 @@ function shuffleArray(array) { */ function inflateArray(array, minSize) { let resultArray = array; - let iterations = Math.ceil(minSize/array.length); + let iterations = Math.ceil(minSize/array.length)-1; for (let i = 0; i < iterations; i++) resultArray = [...resultArray, ...resultArray]; return resultArray @@ -248,7 +260,7 @@ router.get('/', (req, res) => { } }); -router.graphqlResolver = (req) => { +router.graphqlResolver = (req, res) => { let bingoUser = req.session.bingoUser || new BingoUser(); let gameId = req.query.game || bingoUser.game || null; let bingoSession = bingoSessions[gameId]; @@ -268,17 +280,23 @@ router.graphqlResolver = (req) => { }, // mutation createGame: ({input}) => { - let words = input.words; + let words = input.words.filter((el) => { // remove empty strings and non-types from word array + return (!!el && el.length > 0) + }); let size = input.size; - let game = new BingoSession(words, size); - - bingoSessions[game.id] = game; + if (words.length > 0 && size < 10 && size > 0) { + let game = new BingoSession(words, size); - setTimeout(() => { // delete the game after one day - delete bingoSessions[game.id]; - }, 86400000); + bingoSessions[game.id] = game; - return game; + setTimeout(() => { // delete the game after one day + delete bingoSessions[game.id]; + }, 86400000); + return game; + } else { + res.status(400); + return null; + } }, submitBingo: () => { if (checkBingo(bingoUser.grids[gameId])) { @@ -296,20 +314,35 @@ router.graphqlResolver = (req) => { toggleWord: ({input}) => { if (input.word || input.base64Word) { input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); - if (bingoUser.grids[gameId]) + if (bingoUser.grids[gameId]) { toggleHeared(input.base64Word, bingoUser.grids[gameId]); - return bingoUser.grids[gameId]; + return bingoUser.grids[gameId]; + } else { + res.status(400); + } + } else { + res.status(400); } }, setUsername: ({input}) => { if (input.username) { - bingoUser.username = input.username; + bingoUser.username = input.username.substring(0, 30); // only allow 30 characters if (bingoSession) bingoSession.addUser(bingoUser); return bingoUser; } + }, + createFollowupGame: () => { + if (bingoSession) { + if (!bingoSession.followup) + return bingoSession.createFollowup(); + else + return bingoSessions[bingoSession.followup]; + } else { + res.status(400); + } } }; }; diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index a0d3bdf..0cffc18 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -5,19 +5,19 @@ block content div(class='greyover') div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)') input(type='text', id='username-input', placeholder=username) + span Maximum is 30 characters. button(onclick='submitUsername()') Set Username 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='players-container') + h1 Players + 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-layout.pug b/views/bingo/bingo-layout.pug index b3191e6..9549b96 100644 --- a/views/bingo/bingo-layout.pug +++ b/views/bingo/bingo-layout.pug @@ -1,6 +1,7 @@ html head include ../includes/head + title Bingo by Trivernis script(type='text/javascript', src='/javascripts/bingo-web.js') link(rel='stylesheet', href='/sass/bingo/style.sass') body diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug index 2a5228d..e351819 100644 --- a/views/bingo/bingo-submit.pug +++ b/views/bingo/bingo-submit.pug @@ -4,9 +4,10 @@ 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, onkeypress='submitOnEnter(event, submitBingoWords)') + input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=7, onkeypress='submitOnEnter(event, submitBingoWords)') span x span(id='bingo-grid-y', class='number-input') 3 div(class='stretchDiv') button(onclick='submitBingoWords()') Submit + span(id='word-count') Please provide at least 9 phrases: textarea(id='bingo-textarea', placeholder='Bingo Words') diff --git a/views/index.pug b/views/index.pug index 3d63b9a..8b93234 100644 --- a/views/index.pug +++ b/views/index.pug @@ -3,3 +3,4 @@ extends layout block content h1= title p Welcome to #{title} + button(onclick='window.location.href="/bingo"') Bingo diff --git a/views/layout.pug b/views/layout.pug index 15af079..6f767bf 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -2,6 +2,6 @@ doctype html html head title= title - link(rel='stylesheet', href='/stylesheets/style.css') + link(rel='stylesheet', href='/sass/style.sass') body block content