Working frontend

- tweaked backend
- frontend is working again
pull/15/head
Trivernis 6 years ago
parent 5c10862b19
commit 9786167943

@ -59,7 +59,7 @@ async function init() {
app.use('/sass', compileSass({ app.use('/sass', compileSass({
root: './public/stylesheets/sass', root: './public/stylesheets/sass',
sourceMap: true, sourceMap: true,
watchFiles: true, watchFiles: false, // TODO: set true
logToConsole: true logToConsole: true
})); }));
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));

@ -139,6 +139,9 @@ type GridField {
"the column of the field" "the column of the field"
column: Int! column: Int!
"the grid the field belongs to"
grid: BingoGrid
} }
type BingoWord { type BingoWord {

@ -1,6 +1,10 @@
const yaml = require('js-yaml'), const yaml = require('js-yaml'),
fsx = require('fs-extra'); 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: * Parses the `queries.yaml` file in the path. queries.yaml-format:
@ -33,7 +37,17 @@ function readSettings(path) {
return settings; 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, { Object.assign(exports, {
parseSqlYaml: parseSqlYaml, parseSqlYaml: parseSqlYaml,
readSettings: readSettings readSettings: readSettings,
getFileLines: getFileLines
}); });

@ -0,0 +1,11 @@
Sharkinator
Dry River
Cool Dude
Noobmaster69
TheLegend27
BeastMaster64
BitMaster
Angry Koala
Dragonslayer
Goblin Slayer
useless Aqua

@ -53,7 +53,7 @@
"error", "error",
"last" "last"
], ],
"no-await-in-loop": "warn", "no-await-in-loop": "off",
"curly": [ "curly": [
"warn", "warn",
"multi", "multi",

@ -1,15 +1,27 @@
/* eslint-disable no-unused-vars, no-undef */ /* 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} * @returns {string}
*/ */
function getGameParam() { function getLobbyParam() {
let matches = window.location.href.match(/\?game=(\w+)/); let matches = window.location.href.match(/\??&?g=(\d+)/);
if (matches) { if (matches)
return matches[1]; 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 ''; return '';
}
} }
/** /**
@ -43,18 +55,228 @@ async function submitUsername() {
} }
} }
/**
* Creates a lobby and redirects to the lobby.
* @returns {Promise<boolean>}
*/
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<void>}
*/
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<void>}
*/
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<LobbyWrapper.words|*|properties.words|{default, type}|boolean>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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. * 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 winnerDiv = document.createElement('div');
let greyoverDiv = document.createElement('div'); let greyoverDiv = document.createElement('div');
winnerDiv.setAttribute('class', 'popup'); winnerDiv.setAttribute('class', 'popup');
winnerDiv.innerHTML = ` winnerDiv.innerHTML = `
<h1>${name} has won!</h1> <h1>${name} has won!</h1>
<button id="btn-again" onclick="createFollowup()">Again!</button> <button id="button-lobbyreturn" onclick="window.location.reload()">Return to Lobby!</button>
<button id="btn-leave" onclick="window.location.reload()">Leave</button>
`; `;
greyoverDiv.setAttribute('class', 'greyover'); greyoverDiv.setAttribute('class', 'greyover');
//winnerDiv.onclick = () => { //winnerDiv.onclick = () => {
@ -69,18 +291,37 @@ function displayWinner(name) {
* @param errorMessage * @param errorMessage
*/ */
function showError(errorMessage) { function showError(errorMessage) {
let errorDiv = document.createElement('div'); // TODO: Implement
errorDiv.setAttribute('class', 'errorDiv'); }
errorDiv.innerHTML = `<span>${errorMessage}</span>`;
let contCont = document.querySelector('#content-container'); /**
if (contCont) { * Loads information about the rounds winner and the round stats.
contCont.appendChild(errorDiv); * @returns {Promise<void>}
*/
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 { } 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); msgSpan.setAttribute('msg-id', messageObject.id);
if (messageObject.type === "USER") { if (messageObject.type === "USER") {
msgSpan.innerHTML = ` msgSpan.innerHTML = `
<span class="chatUsername">${messageObject.username}:</span> <span class="chatUsername">${messageObject.author.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>`; <span class="chatMessageContent">${messageObject.htmlContent}</span>`;
} else { } else {
msgSpan.innerHTML = ` msgSpan.innerHTML = `
@ -105,11 +346,232 @@ function addChatMessage(messageObject) {
chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom 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 = `<span class="playernameSpan">${player.username}</span>`;
document.querySelector('#player-list').appendChild(playerContainer);
}
/**
* Refreshes the bingo chat
* @returns {Promise<void>}
*/
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<void>}
*/
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<Object>} - 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<void>}
*/
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 = `<span class="bingoWord">
${response.data.bingo.lobby.words.map(x => x.content).join('</span><span class="bingoWord">')}</span>`;
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<void>}
*/
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) { window.addEventListener("unhandledrejection", function (promiseRejectionEvent) {
promiseRejectionEvent.promise.catch(err => console.log(err)); promiseRejectionEvent.promise.catch(err => console.log(err));
showError('Connection problems... Is the server down?'); 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);

@ -8,8 +8,8 @@
@keyframes pulse-opacity @keyframes pulse-opacity
0% 0%
opacity: 0.8 opacity: 0.6
50% 50%
opacity: 1 opacity: 1
100% 100%
opacity: 0.8 opacity: 0.6

@ -1,279 +1,158 @@
@import ../mixins @import ../mixins
@import ../vars @import ../vars
textarea //@media(max-device-width: 641px)
//@media(min-device-width: 641px)
.popup
@include default-element @include default-element
display: block position: fixed
margin: 1rem display: grid
border-radius: 0 height: calc(50% - 1rem)
font-size: 0.8em width: calc(40% - 1rem)
background-color: lighten($primary, 15%) top: 25%
left: 30%
text-align: center
vertical-align: middle
padding: 1rem
z-index: 1000
#word-count button
margin: 1rem margin: 1rem
font-size: 2rem
@media(max-device-width: 641px) .greyover
textarea
height: 80%
width: calc(100% - 2rem)
#words-container
width: 100% width: 100%
height: 80% height: 100%
#content-container position: fixed
grid-template-columns: 0 100% !important z-index: 99
grid-template-rows: 10% 40% 40% 10% !important top: 0
left: 0
#players-container, #chat-container background-color: rgba(0, 0, 0, 0.5)
display: none !important
padding: 0
.errorDiv .playerEntryContainer
grid-column-start: 1 !important @include default-element
grid-column-end: 4 !important padding: 0.5rem
margin: 0 0 1rem
width: calc(100% - 1rem - 2px)
border-radius: 0
color: $primarySurface
border: 1px solid $inactive
#content-container.displayChat .chatMessage
grid-template-columns: 100% 0 !important display: block
grid-template-rows: 0 25% 65% 10% !important padding: 0.2em
#players-container, #chat-container .INFO
display: block !important font-style: italic
color: $inactive
#words-container .ERROR
display: none !important font-weight: bold
color: $error
#hide-player-container-button .chatUsername
display: none color: $inactive
.popup #container-chat
width: calc(100% - 2rem) !important height: calc(100% - 2px)
left: 0 !important #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
#button-container #container-lobby-settings
grid-column-start: 2 !important height: 100%
grid-column-end: 3 !important
#chat-button-container h1
display: inline-block height: 3rem
grid-row-start: 4 margin: 0.5rem
grid-row-end: 4 text-align: center
grid-column-start: 1
grid-column-end: 4
overflow: hidden
margin: 0 0.5rem
button #input-bingo-words
width: 100% width: 100%
margin: 0.5rem 0 height: calc(100% - 7rem)
margin: 0
#chat-container .chatMessage #button-round-start, #button-leave
font-size: 1.2em height: 3rem
@media(min-device-width: 641px)
textarea
height: 80%
width: 50%
#words-container
width: 100% width: 100%
height: 100%
#chat-button-container
display: none
.number-input #bingo-words
width: 4rem padding: 1rem
margin: 1rem width: calc(100% - 2rem)
height: calc(100% - 9rem)
margin: 0
overflow-y: auto
box-shadow: inset 0 0 1rem $primary
#bingoheader .bingoWord
display: table display: list-item
width: 100% list-style: none
div #container-players
display: table-cell height: 100%
text-align: start width: 100%
.stretchDiv h1
text-align: end height: 3rem
margin: 0.5rem
text-align: center
button #player-list
max-width: calc(100% - 2rem) padding: 1rem
padding: 0.7rem 2rem width: calc(100% - 2rem)
height: calc(100% - 6rem)
overflow-y: auto
#words-container #container-grid
display: table display: table
height: calc(100% - 2em)
width: calc(100% - 2em)
padding: 1em
.bingo-word-row .bingoWordRow
display: table-row display: table-row
.bingo-word-panel .bingoWordPanel
@include default-element @include default-element
display: table-cell display: table-cell
padding: 1rem
transition-duration: 0.3s
max-width: 15rem
border-radius: 0
border-collapse: collapse
text-align: center text-align: center
vertical-align: middle vertical-align: middle
user-select: none 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 cursor: pointer
.bingo-word-panel:active .bingoWordPanel[b-sub='true']
background-color: $success
.bingoWordPanel:hover
background-color: $primary background-color: $primary
.bingo-word-panel[b-sub="true"] .bingoWordPanel[b-sub='true']:hover
background-color: forestgreen background-color: mix($primary, $success)
#bingo-button .bingoWordPanel:active
transition-duration: 0.8s background-color: mix($primary, $secondary)
#content-container #container-bingo-button
display: grid
grid-template-columns: 25% 75%
grid-template-rows: 10% 40% 40% 10%
height: 100% height: 100%
width: 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 button
font-size: inherit height: 100%
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% 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 /* main containers */
@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
.greyover
width: 100%
height: 100%
position: fixed
z-index: 99
top: 0
left: 0
background-color: rgba(0, 0, 0, 0.5)
#container-bingo-create #container-bingo-create
display: grid display: grid
grid-template-columns: 10% 80% 10% grid-template: 5% 10% 10% 70% 5% /10% 80% 10%
grid-template-rows: 5% 10% 10% 70% 5%
height: 100% height: 100%
width: 100% width: 100%
@ -296,3 +175,43 @@ textarea
button button
width: 100% 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)

@ -43,3 +43,9 @@
animation-name: pulse-opacity animation-name: pulse-opacity
animation-duration: 5s animation-duration: 5s
animation-iteration-count: infinite animation-iteration-count: infinite
.idle
background-color: $pending !important
animation-name: pulse-opacity
animation-duration: 2s
animation-iteration-count: infinite

@ -53,6 +53,8 @@ input
textarea textarea
background-color: lighten($primary, 15%) background-color: lighten($primary, 15%)
color: $primarySurface
resize: none
a a
color: $secondary color: $secondary

@ -11,7 +11,7 @@ const express = require('express'),
globals = require('../lib/globals'); globals = require('../lib/globals');
let pgPool = globals.pgPool; 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. * Class to manage the bingo data in the database.
@ -373,11 +373,12 @@ class BingoDataManager {
/** /**
* Updates the rounds winner * Updates the rounds winner
* @param roundId {Number} - the id of the round
* @param winnerId {Number} - the id of the winner * @param winnerId {Number} - the id of the winner
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
async setRoundWinner(winnerId) { async setRoundWinner(roundId, winnerId) {
return await this._queryFirstResult(this.queries.setRoundWiner.sql, [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]); 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 * Removes all words of a lobby
* @param lobbyId {Number} - the id of the lobby * @param lobbyId {Number} - the id of the lobby
@ -793,7 +804,7 @@ class RoundWrapper {
async setWinner(winnerId) { async setWinner(winnerId) {
let status = await this.status(); let status = await this.status();
if (status !== "FINISHED") { if (status !== "FINISHED") {
let updateResult = await bdm.setRoundWinner(winnerId); let updateResult = await bdm.setRoundWinner(this.id, winnerId);
if (updateResult) if (updateResult)
await this.setFinished(); await this.setFinished();
return true; return true;
@ -816,11 +827,12 @@ class LobbyWrapper {
/** /**
* Loads information about the lobby if it hasn't been loaded yet * Loads information about the lobby if it hasn't been loaded yet
* @param [force] {Boolean} - forces a data reload
* @returns {Promise<void>} * @returns {Promise<void>}
* @private * @private
*/ */
async _loadLobbyInfo() { async _loadLobbyInfo(force) {
if (!this._infoLoaded) { if (!this._infoLoaded && !force) {
let row = await bdm.getLobbyInfo(this.id); let row = await bdm.getLobbyInfo(this.id);
this._assignProperties(row); this._assignProperties(row);
} }
@ -837,6 +849,7 @@ class LobbyWrapper {
this.grid_size = row.grid_size; this.grid_size = row.grid_size;
this.expire = row.expire; this.expire = row.expire;
this.current_round = row.current_round; this.current_round = row.current_round;
this.last_round = row.last_round;
this._infoLoaded = true; this._infoLoaded = true;
} }
} }
@ -903,7 +916,7 @@ class LobbyWrapper {
let messages = []; let messages = [];
for (let row of rows) for (let row of rows)
messages.push(new MessageWrapper(row)); messages.push(new MessageWrapper(row));
return messages; return messages.reverse();
} }
/** /**
@ -999,6 +1012,27 @@ class LobbyWrapper {
await bdm.addWordToLobby(this.id, word); await bdm.addWordToLobby(this.id, word);
} }
} }
/**
* Adds a player to the lobby.
* @param playerId
* @returns {Promise<void>}
*/
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<boolean>}
*/
async roundActive() {
let currentRound = await this.currentRound();
return currentRound && (await currentRound.status()) === 'ACTIVE';
}
} }
@ -1124,6 +1158,64 @@ function checkBingo(fg) {
return diagonalBingo || verticalCheck || horizontalCheck; return diagonalBingo || verticalCheck || horizontalCheck;
} }
/**
* Gets player data for a lobby
* @param lobbyWrapper
* @returns {Promise<Array>}
*/
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<Array>}
*/
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 // -- Router stuff
@ -1140,27 +1232,35 @@ router.use(async (req, res, next) => {
next(); next();
}); });
router.get('/', (req, res) => { router.get('/', async (req, res) => {
let bingoUser = req.session.bingoUser; let playerId = req.session.bingoPlayerId;
if (!playerId)
req.session.bingoPlayerId = playerId = (await bdm.addPlayer(shuffleArray(playerNames)[0])).id;
if (req.query.g) { if (req.query.g) {
let lobbyId = req.query.g; let lobbyId = req.query.g;
let lobbyWrapper = new LobbyWrapper(lobbyId);
if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { if (!(await lobbyWrapper.roundActive())) {
bingoUser.game = gameId; if (!await lobbyWrapper.hasPlayer(playerId))
let bingoSession = bingoSessions[gameId]; await lobbyWrapper.addPlayer(playerId);
if (!bingoSession.users[bingoUser.id]) let playerData = await getPlayerData(lobbyWrapper);
bingoSession.addUser(bingoUser); let words = await getWordsData(lobbyWrapper);
let admin = await lobbyWrapper.admin();
if (!bingoUser.grids[gameId]) res.render('bingo/bingo-lobby', {
bingoUser.grids[gameId] = generateWordGrid(bingoSession.gridSize, bingoSession.words); players: playerData,
isAdmin: (playerId === admin.id),
res.render('bingo/bingo-game', { words: words,
grid: bingoUser.grids[gameId].fieldGrid, wordString: words.join('\n')});
username: bingoUser.username, } else {
players: bingoSession.players() 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 { } else {
res.render('bingo/bingo-submit'); let playerData = await getPlayerData(lobbyWrapper);
let admin = await lobbyWrapper.admin();
res.render('bingo/bingo-lobby', {players: playerData, isAdmin: (playerId === admin.id)});
}
} }
} else { } else {
res.render('bingo/bingo-create'); res.render('bingo/bingo-create');
@ -1174,7 +1274,8 @@ router.graphqlResolver = async (req, res) => {
return { return {
// queries // queries
lobby: ({id}) => { lobby: async ({id}) => {
await bdm.updateLobbyExpiration(id);
return new LobbyWrapper(id); return new LobbyWrapper(id);
}, },
player: ({id}) => { player: ({id}) => {

@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS bingo.words (
CREATE TABLE IF NOT EXISTS bingo.messages ( CREATE TABLE IF NOT EXISTS bingo.messages (
id serial UNIQUE PRIMARY KEY, id serial UNIQUE PRIMARY KEY,
content varchar(255) NOT NULL, 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, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE,
type varchar(8) DEFAULT 'USER' NOT NULL, type varchar(8) DEFAULT 'USER' NOT NULL,
created timestamp DEFAULT NOW() created timestamp DEFAULT NOW()
@ -68,3 +68,7 @@ CREATE TABLE IF NOT EXISTS bingo.grid_words (
submitted boolean DEFAULT false, submitted boolean DEFAULT false,
PRIMARY KEY (grid_id, grid_row, grid_column) PRIMARY KEY (grid_id, grid_row, grid_column)
); );
-- altering
ALTER TABLE bingo.messages ALTER COLUMN player_id DROP NOT NULL;

@ -239,3 +239,10 @@ getWordsForGridId:
# - {String} - the content of the message # - {String} - the content of the message
addUserMessage: addUserMessage:
sql: INSERT INTO bingo.messages (player_id, lobby_id, content) VALUES ($1, $2, $3) RETURNING *; 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 *;

@ -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")

@ -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();

@ -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

@ -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();

@ -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)
Loading…
Cancel
Save