diff --git a/app.js b/app.js index 771587b..3f9a49c 100644 --- a/app.js +++ b/app.js @@ -59,7 +59,7 @@ async function init() { app.use('/sass', compileSass({ root: './public/stylesheets/sass', sourceMap: true, - watchFiles: true, + watchFiles: false, // TODO: set true logToConsole: true })); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index fa3d9df..9463835 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -139,6 +139,9 @@ type GridField { "the column of the field" column: Int! + + "the grid the field belongs to" + grid: BingoGrid } type BingoWord { diff --git a/lib/utils.js b/lib/utils.js index dbc8264..69e3835 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,10 @@ const yaml = require('js-yaml'), fsx = require('fs-extra'); +String.prototype.replaceAll = function(search, replacement) { + let target = this; + return target.replace(new RegExp(search, 'g'), replacement); +}; /** * Parses the `queries.yaml` file in the path. queries.yaml-format: @@ -33,7 +37,17 @@ function readSettings(path) { return settings; } +/** + * Returns all lines of a file as array + * @param fname {String} - the name of the file + * @returns {string[]} + */ +function getFileLines(fname) { + return fsx.readFileSync(fname).toString().replaceAll('\r\n', '\n').split('\n'); +} + Object.assign(exports, { parseSqlYaml: parseSqlYaml, - readSettings: readSettings + readSettings: readSettings, + getFileLines: getFileLines }); diff --git a/misc/usernames.txt b/misc/usernames.txt new file mode 100644 index 0000000..0ba10d3 --- /dev/null +++ b/misc/usernames.txt @@ -0,0 +1,11 @@ +Sharkinator +Dry River +Cool Dude +Noobmaster69 +TheLegend27 +BeastMaster64 +BitMaster +Angry Koala +Dragonslayer +Goblin Slayer +useless Aqua diff --git a/package.json b/package.json index 6b1c6db..48f6019 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "error", "last" ], - "no-await-in-loop": "warn", + "no-await-in-loop": "off", "curly": [ "warn", "multi", diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 7babd77..b9ad5fa 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -1,15 +1,27 @@ /* eslint-disable no-unused-vars, no-undef */ /** - * Returns the value of the url-param 'game' + * Returns the value of the url-param 'g' * @returns {string} */ -function getGameParam() { - let matches = window.location.href.match(/\?game=(\w+)/); - if (matches) { +function getLobbyParam() { + let matches = window.location.href.match(/\??&?g=(\d+)/); + if (matches) return matches[1]; - } else { + else + return ''; + +} + +/** + * REturns the value of the r url param + * @returns {string} + */ +function getRoundParam() { + let matches = window.location.href.match(/\??&?r=(\d+)/); + if (matches) + return matches[1]; + else return ''; - } } /** @@ -43,18 +55,228 @@ async function submitUsername() { } } +/** + * Creates a lobby and redirects to the lobby. + * @returns {Promise} + */ +async function createLobby() { + let response = await postGraphqlQuery(` + mutation { + bingo { + createLobby { + id + } + } + } + `); + if (response.status === 200 && response.data.bingo.createLobby) { + insertParam('g', response.data.bingo.createLobby.id); + return true; + } else { + showError('Failed to create Lobby. HTTP ERROR: ' + response.status); + console.error(response); + return false; + } +} + +/** + * Lets the player leave the lobby + * @returns {Promise} + */ +async function leaveLobby() { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + leave + } + } + } + `, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + insertParam('g', ''); + } else { + showError('Failed to leave lobby'); + console.error(response); + } +} + +/** + * Sends a message to the chat + * @returns {Promise} + */ +async function sendChatMessage() { + let messageInput = document.querySelector('#chat-input'); + if (messageInput.value && messageInput.value.length > 0) { + let message = messageInput.value; + messageInput.value = ''; + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!, $message:String!){ + bingo { + mutateLobby(id:$lobbyId) { + sendMessage(message:$message) { + id + htmlContent + type + author { + username + } + } + } + } + }`, {message: message, lobbyId: getLobbyParam()}); + if (response.status === 200) { + addChatMessage(response.data.bingo.mutateLobby.sendMessage); + } else { + messageInput.value = message; + console.error(response); + showError('Error when sending message.'); + } + } +} + +/** + * Sets the words for the lobby + * @param words + * @returns {Promise} + */ +async function setLobbyWords(words) { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!, $words:[String!]!){ + bingo { + mutateLobby(id:$lobbyId) { + setWords(words:$words) { + words { + content + } + } + } + } + }`, {lobbyId: getLobbyParam(), words: words}); + if (response.status === 200) { + return response.data.bingo.mutateLobby.setWords.words; + } else { + console.error(response); + showError('Error when setting lobby words.'); + } +} + +/** + * Starts a new round of bingo + * @returns {Promise} + */ +async function startRound() { + let words = getLobbyWords(); + let resultWords = await setLobbyWords(words); + textinput.value = resultWords.map(x => x.content).join('\n'); + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + startRound { + id + } + } + } + }`, {lobbyId: getLobbyParam()}); + + if (response.status === 200) { + insertParam('r', response.data.bingo.mutateLobby.startRound.id); + } else { + console.error(response); + showError('Error when starting round.'); + } +} + +/** + * Returns the words of the lobby word input. + * @returns {string[]} + */ +function getLobbyWords() { + let textinput = document.querySelector('#input-bingo-words'); + let words = textinput.value.replace(/[<>]/g, '').split('\n').filter((el) => { + return (!!el && el.length > 0); // remove empty strings and non-types from word array + }); + return words; +} + +/** + * Submits the toggle of a bingo field + * @param wordPanel + * @returns {Promise} + */ +async function submitFieldToggle(wordPanel) { + let row = Number(wordPanel.getAttribute('b-row')); + let column = Number(wordPanel.getAttribute('b-column')); + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!, $row:Int!, $column:Int!){ + bingo { + mutateLobby(id:$lobbyId) { + toggleGridField(location:{row:$row, column:$column}) { + submitted + grid { + bingo + } + } + } + } + }`, {lobbyId: getLobbyParam(), row: row, column: column}); + + if (response.status === 200) { + wordPanel.setAttribute('b-sub', response.data.bingo.mutateLobby.toggleGridField.submitted); + if (response.data.bingo.mutateLobby.toggleGridField.grid.bingo) + document.querySelector('#container-bingo-button').setAttribute('class', ''); + else + document.querySelector('#container-bingo-button').setAttribute('class', 'hidden'); + } else { + console.error(response); + showError('Error when submitting field toggle'); + } +} + +/** + * Submits bingo + * @returns {Promise} + */ +async function submitBingo() { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + submitBingo { + winner { + id + username + } + status + start + finish + } + } + } + }`, {lobbyId: getLobbyParam()}); + + if (response.status === 200 && response.data.bingo.mutateLobby.submitBingo) { + let round = response.data.bingo.mutateLobby.submitBingo; + displayWinner(round); + } else { + console.error(response); + showError('Failed to submit bingo'); + } +} + /** * Displays the winner of the game in a popup. - * @param name {String} - the name of the winner + * @param roundInfo {Object} - the round object as returned by graphql */ -function displayWinner(name) { +function displayWinner(roundInfo) { + let name = roundInfo.winner.username; let winnerDiv = document.createElement('div'); let greyoverDiv = document.createElement('div'); winnerDiv.setAttribute('class', 'popup'); winnerDiv.innerHTML = `

${name} has won!

- - + `; greyoverDiv.setAttribute('class', 'greyover'); //winnerDiv.onclick = () => { @@ -69,18 +291,37 @@ function displayWinner(name) { * @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); + // TODO: Implement +} + +/** + * Loads information about the rounds winner and the round stats. + * @returns {Promise} + */ +async function loadWinnerInfo() { + let response = await postGraphqlQuery(` + query($lobbyId:ID!) { + bingo { + lobby(id:$lobbyId) { + currentRound { + status + winner { + id + username + } + start + finish + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let roundInfo = response.data.bingo.lobby.currentRound; + displayWinner(roundInfo); } else { - alert(errorMessage); + console.error(response); + showError('Failed to get round information'); } - setTimeout(() => { - errorDiv.remove(); - }, 10000); } /** @@ -93,7 +334,7 @@ function addChatMessage(messageObject) { msgSpan.setAttribute('msg-id', messageObject.id); if (messageObject.type === "USER") { msgSpan.innerHTML = ` - ${messageObject.username}: + ${messageObject.author.username}: ${messageObject.htmlContent}`; } else { msgSpan.innerHTML = ` @@ -105,11 +346,232 @@ function addChatMessage(messageObject) { chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom } +/** + * Adds a player to the player view + * @param player + */ +function addPlayer(player) { + let playerContainer = document.createElement('div'); + playerContainer.setAttribute('class', 'playerEntryContainer'); + playerContainer.setAttribute('b-pid', player.id); + playerContainer.innerHTML = `${player.username}`; + document.querySelector('#player-list').appendChild(playerContainer); +} + +/** + * Refreshes the bingo chat + * @returns {Promise} + */ +async function refreshChat() { + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + messages { + id + type + htmlContent + author { + username + } + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let messages = response.data.bingo.lobby.messages; + for (let message of messages) + if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) + addChatMessage(message); + } else { + showError('Failed to refresh messages'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh messages'); + console.error(err); + } + console.log('Refresh Chat'); +} + +/** + * Refreshes the player list + * @returns {Promise} + */ +async function refreshPlayers() { + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + players { + id + username + wins(lobbyId:$lobbyId) + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let players = response.data.bingo.lobby.players; + for (let player of players) + if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) + addPlayer(player); + } else { + showError('Failed to refresh players'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh players'); + console.error(err); + } +} + +/** + * Removes players that are not existent in the player array + * @param players {Array} - player id response of graphql + */ +function removeLeftPlayers(players) { + for (let playerEntry of document.querySelectorAll('.playerEntryContainer')) + if (!players.find(x => (x.id === playerEntry.getAttribute('b-pid')))) + playerEntry.remove(); +} + +/** + * Refreshes if a player-refresh is needed. + * Removes players that are not in the lobby anyomre. + * @param players + */ +function checkPlayerRefresh(players) { + let playerRefresh = false; + removeLeftPlayers(players); + for (let player of players) + if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) + playerRefresh = true; + if (playerRefresh) + refreshPlayers(); +} + +/** + * Checks if messages need to be refreshed and does it if it needs to. + * @param messages + */ +function checkMessageRefresh(messages) { + let messageRefresh = false; + for (let message of messages) + if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) + messageRefresh = true; + if (messageRefresh) + refreshChat(); +} + +/** + * refreshes the lobby and calls itself with a timeout + * @returns {Promise} + */ +async function refreshLobby() { + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + players { + id + } + messages { + id + } + currentRound { + id + } + words { + content + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let {players, messages, currentRound} = response.data.bingo.lobby; + checkPlayerRefresh(players); + checkMessageRefresh(messages); + let wordContainer = document.querySelector('#bingo-words'); + + if (wordContainer) + wordContainer.innerHTML = ` + ${response.data.bingo.lobby.words.map(x => x.content).join('')}`; + + if (currentRound && currentRound.id && Number(currentRound.id) !== Number(getRoundParam())) + insertParam('r', currentRound.id); + + } else { + showError('Failed to refresh lobby'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh lobby'); + console.error(err); + } finally { + setTimeout(refreshLobby, 1000); + } +} + +/** + * Checks the status of the lobby and the current round. + * @returns {Promise} + */ +async function refreshRound() { + let roundOver = false; + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!) { + bingo { + lobby(id:$lobbyId) { + players { + id + } + messages { + id + } + currentRound { + id + status + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let {players, messages, currentRound} = response.data.bingo.lobby; + + checkPlayerRefresh(players); + checkMessageRefresh(messages); + if (!currentRound || currentRound.status === "FINISHED") { + roundOver = true; + await loadWinnerInfo(); + } + } else { + showError('Failed to refresh round'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh round'); + console.error(err); + } finally { + if (!roundOver) + setTimeout(refreshRound, 1000); + } +} window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { promiseRejectionEvent.promise.catch(err => console.log(err)); showError('Connection problems... Is the server down?'); }); -window.onload = () => { -}; +// prevent ctrl + s +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()); + } +}, false); diff --git a/public/stylesheets/sass/animations.sass b/public/stylesheets/sass/animations.sass index 69245b0..11717a4 100644 --- a/public/stylesheets/sass/animations.sass +++ b/public/stylesheets/sass/animations.sass @@ -8,8 +8,8 @@ @keyframes pulse-opacity 0% - opacity: 0.8 + opacity: 0.6 50% opacity: 1 100% - opacity: 0.8 + opacity: 0.6 diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 9596e6a..fd401eb 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -1,279 +1,158 @@ @import ../mixins @import ../vars -textarea - @include default-element - display: block - margin: 1rem - border-radius: 0 - font-size: 0.8em - background-color: lighten($primary, 15%) +//@media(max-device-width: 641px) -#word-count - margin: 1rem +//@media(min-device-width: 641px) -@media(max-device-width: 641px) - textarea - height: 80% - width: calc(100% - 2rem) - #words-container - width: 100% - height: 80% - #content-container - grid-template-columns: 0 100% !important - grid-template-rows: 10% 40% 40% 10% !important +.popup + @include default-element + position: fixed + display: grid + height: calc(50% - 1rem) + width: calc(40% - 1rem) + top: 25% + left: 30% + text-align: center + vertical-align: middle + padding: 1rem + z-index: 1000 - #players-container, #chat-container - display: none !important - padding: 0 + button + margin: 1rem + font-size: 2rem - .errorDiv - grid-column-start: 1 !important - grid-column-end: 4 !important +.greyover + width: 100% + height: 100% + position: fixed + z-index: 99 + top: 0 + left: 0 + background-color: rgba(0, 0, 0, 0.5) - #content-container.displayChat - grid-template-columns: 100% 0 !important - grid-template-rows: 0 25% 65% 10% !important +.playerEntryContainer + @include default-element + padding: 0.5rem + margin: 0 0 1rem + width: calc(100% - 1rem - 2px) + border-radius: 0 + color: $primarySurface + border: 1px solid $inactive - #players-container, #chat-container - display: block !important +.chatMessage + display: block + padding: 0.2em - #words-container - display: none !important + .INFO + font-style: italic + color: $inactive - #hide-player-container-button - display: none + .ERROR + font-weight: bold + color: $error - .popup - width: calc(100% - 2rem) !important - left: 0 !important + .chatUsername + color: $inactive - #button-container - grid-column-start: 2 !important - grid-column-end: 3 !important +#container-chat + height: calc(100% - 2px) + #chat-content + height: calc(100% - 3rem) + width: 100% + overflow-y: auto + border: 1px solid $inactive + box-shadow: inset 0 0 1rem $primary + #chat-input + width: 100% + height: 3rem - #chat-button-container - display: inline-block - grid-row-start: 4 - grid-row-end: 4 - grid-column-start: 1 - grid-column-end: 4 - overflow: hidden - margin: 0 0.5rem +#container-lobby-settings + height: 100% - button - width: 100% - margin: 0.5rem 0 + h1 + height: 3rem + margin: 0.5rem + text-align: center - #chat-container .chatMessage - font-size: 1.2em + #input-bingo-words + width: 100% + height: calc(100% - 7rem) + margin: 0 -@media(min-device-width: 641px) - textarea - height: 80% - width: 50% - #words-container + #button-round-start, #button-leave + height: 3rem width: 100% - height: 100% - #chat-button-container - display: none -.number-input - width: 4rem - margin: 1rem + #bingo-words + padding: 1rem + width: calc(100% - 2rem) + height: calc(100% - 9rem) + margin: 0 + overflow-y: auto + box-shadow: inset 0 0 1rem $primary -#bingoheader - display: table - width: 100% + .bingoWord + display: list-item + list-style: none - div - display: table-cell - text-align: start +#container-players + height: 100% + width: 100% - .stretchDiv - text-align: end + h1 + height: 3rem + margin: 0.5rem + text-align: center - button - max-width: calc(100% - 2rem) - padding: 0.7rem 2rem + #player-list + padding: 1rem + width: calc(100% - 2rem) + height: calc(100% - 6rem) + overflow-y: auto -#words-container +#container-grid display: table + height: calc(100% - 2em) + width: calc(100% - 2em) + padding: 1em - .bingo-word-row + .bingoWordRow display: table-row - .bingo-word-panel + .bingoWordPanel @include default-element display: table-cell - padding: 1rem - transition-duration: 0.3s - max-width: 15rem - border-radius: 0 - 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%) cursor: pointer - .bingo-word-panel:active + .bingoWordPanel[b-sub='true'] + background-color: $success + + .bingoWordPanel:hover background-color: $primary - .bingo-word-panel[b-sub="true"] - background-color: forestgreen + .bingoWordPanel[b-sub='true']:hover + background-color: mix($primary, $success) -#bingo-button - transition-duration: 0.8s + .bingoWordPanel:active + background-color: mix($primary, $secondary) -#content-container - display: grid - grid-template-columns: 25% 75% - grid-template-rows: 10% 40% 40% 10% +#container-bingo-button height: 100% width: 100% - div - overflow: auto - - #button-container - grid-column-start: 1 - grid-column-end: 1 - grid-row-start: 1 - grid-row-end: 1 - display: grid - 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: 3 - - h1 - margin: 0 0 1rem 0 - - #words-container - grid-column-start: 2 - grid-column-end: 3 - grid-row-start: 2 - grid-row-end: 4 - - #chat-container - grid-column-start: 1 - grid-column-end: 1 - grid-row-start: 3 - grid-row-end: 4 - height: calc(100% - 3px) - border: 1px solid $inactive - margin: 0 0.5rem - word-break: break-word - - #chat-content - height: calc(100% - 2.5rem) - background-color: $primary - overflow: auto - font-size: 0.8em - - .chatMessage - display: list-item - padding: 0.2rem - - .chatUsername - color: $inactive - - .ERROR - color: $error - - .INFO - color: $inactive - font-style: italic - - .chatMessageContent - img - width: 100% - height: auto - transition-duration: 0.5s - border-radius: 0.5em - img:hover - border-radius: 0 - - #chat-input - width: 100% - margin: 0 0 0 0 - height: 2.5rem - border-radius: 0 - - .errorDiv - grid-column-start: 2 - grid-column-end: 3 - grid-row-start: 4 - grid-row-end: 4 - background-color: $error - text-align: center - margin: 0.75rem 0 - border-radius: 1rem - height: calc(100% - 1.5rem) - display: table - - span - display: table-cell - font-size: 1.8rem - vertical-align: middle - -.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 - position: fixed - display: grid - height: calc(50% - 1rem) - width: calc(40% - 1rem) - top: 25% - left: 30% - text-align: center - vertical-align: middle - padding: 1rem - z-index: 1000 - button - margin: 1rem - font-size: 2rem + height: 100% + width: 100% -.greyover - width: 100% - height: 100% - position: fixed - z-index: 99 - top: 0 - left: 0 - background-color: rgba(0, 0, 0, 0.5) +/* main containers */ #container-bingo-create display: grid - grid-template-columns: 10% 80% 10% - grid-template-rows: 5% 10% 10% 70% 5% + grid-template: 5% 10% 10% 70% 5% /10% 80% 10% height: 100% width: 100% @@ -296,3 +175,43 @@ textarea button width: 100% + +#container-bingo-lobby + display: grid + grid-template: 5% 5% 85% 5% / 5% 30% 30% 30% 5% + height: 100% + width: 100% + + #lobby-title + @include gridPosition(2, 3, 2, 5) + margin: auto + + #container-players + @include gridPosition(3, 4, 2, 3) + background-color: lighten($primary, 5%) + + #container-lobby-settings + @include gridPosition(3, 4, 3, 4) + background-color: lighten($primary, 5%) + + #container-chat + @include gridPosition(3, 4, 4, 5) + background-color: lighten($primary, 5%) + +#container-bingo-round + display: grid + height: 100% + width: 100% + grid-template: 10% 42.5% 42.5% 5% / 25% 75% + + #container-players + @include gridPosition(2, 3, 1, 2) + + #container-chat + @include gridPosition(3, 4, 1, 2) + + #container-grid + @include gridPosition(2, 4, 2, 3) + + #container-bingo-button + @include gridPosition(1, 2, 1, 2) diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index e73f69f..c38e044 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -43,3 +43,9 @@ animation-name: pulse-opacity animation-duration: 5s animation-iteration-count: infinite + +.idle + background-color: $pending !important + animation-name: pulse-opacity + animation-duration: 2s + animation-iteration-count: infinite diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 16f0db6..2628fa9 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -53,6 +53,8 @@ input textarea background-color: lighten($primary, 15%) + color: $primarySurface + resize: none a color: $secondary diff --git a/routes/bingo.js b/routes/bingo.js index ea1b7f7..127c4f6 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -11,7 +11,7 @@ const express = require('express'), globals = require('../lib/globals'); let pgPool = globals.pgPool; -let bingoSessions = {}; +let playerNames = utils.getFileLines('./misc/usernames.txt').filter(x => (x && x.length > 0)); /** * Class to manage the bingo data in the database. @@ -373,11 +373,12 @@ class BingoDataManager { /** * Updates the rounds winner + * @param roundId {Number} - the id of the round * @param winnerId {Number} - the id of the winner * @returns {Promise<*>} */ - async setRoundWinner(winnerId) { - return await this._queryFirstResult(this.queries.setRoundWiner.sql, [winnerId]); + async setRoundWinner(roundId, winnerId) { + return await this._queryFirstResult(this.queries.setRoundWinner.sql, [roundId, winnerId]); } /** @@ -391,6 +392,16 @@ class BingoDataManager { return await this._queryFirstResult(this.queries.addUserMessage.sql, [playerId, lobbyId, messageContent]); } + /** + * Adds a message of type "INFO" to the lobby + * @param lobbyId {Number} - the id of the lobby + * @param messageContent {String} - the content of the info message + * @returns {Promise<*>} + */ + async addInfoMessage(lobbyId, messageContent) { + return await this._queryFirstResult(this.queries.addInfoMessage.sql, [lobbyId, messageContent]); + } + /** * Removes all words of a lobby * @param lobbyId {Number} - the id of the lobby @@ -793,7 +804,7 @@ class RoundWrapper { async setWinner(winnerId) { let status = await this.status(); if (status !== "FINISHED") { - let updateResult = await bdm.setRoundWinner(winnerId); + let updateResult = await bdm.setRoundWinner(this.id, winnerId); if (updateResult) await this.setFinished(); return true; @@ -816,11 +827,12 @@ class LobbyWrapper { /** * Loads information about the lobby if it hasn't been loaded yet + * @param [force] {Boolean} - forces a data reload * @returns {Promise} * @private */ - async _loadLobbyInfo() { - if (!this._infoLoaded) { + async _loadLobbyInfo(force) { + if (!this._infoLoaded && !force) { let row = await bdm.getLobbyInfo(this.id); this._assignProperties(row); } @@ -837,6 +849,7 @@ class LobbyWrapper { this.grid_size = row.grid_size; this.expire = row.expire; this.current_round = row.current_round; + this.last_round = row.last_round; this._infoLoaded = true; } } @@ -903,7 +916,7 @@ class LobbyWrapper { let messages = []; for (let row of rows) messages.push(new MessageWrapper(row)); - return messages; + return messages.reverse(); } /** @@ -999,6 +1012,27 @@ class LobbyWrapper { await bdm.addWordToLobby(this.id, word); } } + + /** + * Adds a player to the lobby. + * @param playerId + * @returns {Promise} + */ + async addPlayer(playerId) { + await bdm.addPlayerToLobby(playerId, this.id); + let username = await new PlayerWrapper(playerId).username(); + await bdm.addInfoMessage(this.id, `${username} joined.`); + await this._loadLobbyInfo(true); + } + + /** + * Returns if the lobby is in an active round + * @returns {Promise} + */ + async roundActive() { + let currentRound = await this.currentRound(); + return currentRound && (await currentRound.status()) === 'ACTIVE'; + } } @@ -1124,6 +1158,64 @@ function checkBingo(fg) { return diagonalBingo || verticalCheck || horizontalCheck; } +/** + * Gets player data for a lobby + * @param lobbyWrapper + * @returns {Promise} + */ +async function getPlayerData(lobbyWrapper) { + let playerData = []; + + for (let player of await lobbyWrapper.players()) + playerData.push({ + id: player.id, + wins: await player.wins({lobbyId: lobbyWrapper.id}), + username: await player.username()} + ); + return playerData; +} + +/** + * Gets data for all words of a lobby + * @param lobbyWrapper + * @returns {Promise} + */ +async function getWordsData(lobbyWrapper) { + let wordList = []; + + for (let word of await lobbyWrapper.words()) + wordList.push(await word.content()); + return wordList; +} + +/** + * Returns a completely resolved grid + * @param lobbyId + * @param playerId + * @returns {Promise<{bingo: boolean, fields: Array}>} + */ +async function getGridData(lobbyId, playerId) { + let playerWrapper = new PlayerWrapper(playerId); + let lobbyWrapper = new LobbyWrapper(lobbyId); + let grid = await playerWrapper.grid({lobbyId: lobbyId}); + let fields = await grid.fields(); + let fieldGrid = []; + + for (let i = 0; i < await lobbyWrapper.gridSize(); i++) { + fieldGrid[i] = []; + for (let j = 0; j < await lobbyWrapper.gridSize(); j++) { + let field = fields.find(x => (x.row === i && x.column === j)) + fieldGrid[i][j] = { + row: field.row, + column: field.column, + word: await field.word.content(), + submitted: field.submitted + }; + } + } + + return {fields: fieldGrid, bingo: await grid.bingo()}; +} // -- Router stuff @@ -1140,27 +1232,35 @@ router.use(async (req, res, next) => { next(); }); -router.get('/', (req, res) => { - let bingoUser = req.session.bingoUser; +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 lobbyId = req.query.g; - - if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { - bingoUser.game = gameId; - let bingoSession = bingoSessions[gameId]; - if (!bingoSession.users[bingoUser.id]) - bingoSession.addUser(bingoUser); - - if (!bingoUser.grids[gameId]) - bingoUser.grids[gameId] = generateWordGrid(bingoSession.gridSize, bingoSession.words); - - res.render('bingo/bingo-game', { - grid: bingoUser.grids[gameId].fieldGrid, - username: bingoUser.username, - players: bingoSession.players() - }); + let lobbyWrapper = new LobbyWrapper(lobbyId); + + if (!(await lobbyWrapper.roundActive())) { + if (!await lobbyWrapper.hasPlayer(playerId)) + await lobbyWrapper.addPlayer(playerId); + let playerData = await getPlayerData(lobbyWrapper); + let words = await getWordsData(lobbyWrapper); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-lobby', { + players: playerData, + isAdmin: (playerId === admin.id), + words: words, + wordString: words.join('\n')}); } else { - res.render('bingo/bingo-submit'); + 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}); + } else { + let playerData = await getPlayerData(lobbyWrapper); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-lobby', {players: playerData, isAdmin: (playerId === admin.id)}); + } } } else { res.render('bingo/bingo-create'); @@ -1174,7 +1274,8 @@ router.graphqlResolver = async (req, res) => { return { // queries - lobby: ({id}) => { + lobby: async ({id}) => { + await bdm.updateLobbyExpiration(id); return new LobbyWrapper(id); }, player: ({id}) => { diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql index b14176d..e84d6b3 100644 --- a/sql/bingo/createBingoTables.sql +++ b/sql/bingo/createBingoTables.sql @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS bingo.words ( CREATE TABLE IF NOT EXISTS bingo.messages ( id serial UNIQUE PRIMARY KEY, content varchar(255) NOT NULL, - player_id serial references bingo.players(id) ON DELETE SET NULL, + player_id integer, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, type varchar(8) DEFAULT 'USER' NOT NULL, created timestamp DEFAULT NOW() @@ -68,3 +68,7 @@ CREATE TABLE IF NOT EXISTS bingo.grid_words ( submitted boolean DEFAULT false, PRIMARY KEY (grid_id, grid_row, grid_column) ); + +-- altering + +ALTER TABLE bingo.messages ALTER COLUMN player_id DROP NOT NULL; diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index bb710f3..2638aa2 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -239,3 +239,10 @@ getWordsForGridId: # - {String} - the content of the message addUserMessage: sql: INSERT INTO bingo.messages (player_id, lobby_id, content) VALUES ($1, $2, $3) RETURNING *; + +# inserts a info message +# params: +# - {Number} - the id of the lobby +# - {String} - the content of the message +addInfoMessage: + sql: INSERT INTO bingo.messages (type, lobby_id, content) VALUES ('INFO', $1, $2) RETURNING *; diff --git a/views/bingo/bingo-chat.pug b/views/bingo/bingo-chat.pug new file mode 100644 index 0000000..74027b5 --- /dev/null +++ b/views/bingo/bingo-chat.pug @@ -0,0 +1,4 @@ +div(id='container-chat') + //h1(id='chat-header') Chat + div(id='chat-content') + input(id='chat-input' type='text', placeholder='send message', onkeypress='submitOnEnter(event, sendChatMessage)' maxlength="250") diff --git a/views/bingo/bingo-lobby.pug b/views/bingo/bingo-lobby.pug new file mode 100644 index 0000000..2ae7641 --- /dev/null +++ b/views/bingo/bingo-lobby.pug @@ -0,0 +1,19 @@ +extends bingo-layout + +block content + div(id='container-bingo-lobby') + h1(id='lobby-title') Bingo Lobby + include bingo-players + div(id='container-lobby-settings') + h1 Words + if isAdmin + textarea(id='input-bingo-words')= wordString + button(id='button-round-start' onclick='startRound()') Start Round + else + div(id='bingo-words') + for word in words + span(class='bingoWord')= word + button(id='button-leave' onclick='leaveLobby()') Leave + include bingo-chat + + script(type='text/javascript') refreshLobby(); diff --git a/views/bingo/bingo-players.pug b/views/bingo/bingo-players.pug new file mode 100644 index 0000000..aefa5d3 --- /dev/null +++ b/views/bingo/bingo-players.pug @@ -0,0 +1,6 @@ +div(id='container-players') + h1 Players + div(id='player-list') + each player in players + div(class='playerEntryContainer', b-pid=`${player.id}`) + span(class='playerNameSpan')= player.username diff --git a/views/bingo/bingo-round.pug b/views/bingo/bingo-round.pug new file mode 100644 index 0000000..45aeb0a --- /dev/null +++ b/views/bingo/bingo-round.pug @@ -0,0 +1,24 @@ +include bingo-layout + +block content + div(id='container-bingo-round') + include bingo-players + include bingo-chat + if grid.bingo + div(id='container-bingo-button') + button(id='bingo-button' onclick='submitBingo()') Bingo! + else + div(id='container-bingo-button' class='hidden') + button(id='bingo-button' onclick='submitBingo()') Bingo! + div(id='container-grid') + each val in grid.fields + div(class='bingoWordRow') + each field in val + div( + class='bingoWordPanel' + onclick=`submitFieldToggle(this)` + b-row=field.row + b-column=field.column + b-sub=`${field.submitted}`) + span= field.word + script(type='text/javascript') refreshRound(); diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug deleted file mode 100644 index db8c533..0000000 --- a/views/bingo/bingo-submit.pug +++ /dev/null @@ -1,13 +0,0 @@ -extends bingo-layout - -block content - div(id='bingoform') - div(id='bingoheader') - div - 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 (max 10,000)', maxlength=1000000)