Added bingo startpage (wip)

- added css for startpage (wip)
- added file for css animations
- added pug file for bingo starpage
pull/15/head
Trivernis 6 years ago
parent 9b31ac80e0
commit 5c10862b19

@ -20,15 +20,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- sql scripts for bingo - sql scripts for bingo
- data management class for bingo - data management class for bingo
- libs with utils and global variables - libs with utils and global variables
- css for startpage (wip)
- file for css animations
- pug file for startpage
## Changed ## Changed
- changed export of `app.js` to the asynchronous init function that returns the app object - 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` - `bin/www` now calls the init function of `app.js`
- graphql api
### Removed ### Removed
- sqlite3 sesssion storage - sqlite3 sesssion storage
- old frontend
### Fixed ### Fixed

@ -5,272 +5,41 @@
*/ */
function getGameParam() { function getGameParam() {
let matches = window.location.href.match(/\?game=(\w+)/); let matches = window.location.href.match(/\?game=(\w+)/);
if (matches) if (matches) {
return matches[1]; return matches[1];
else
return '';
}
/**
* Submits the bingo words to create a game
* @returns {Promise<void>}
*/
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;
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<void>}
*/
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 { } else {
showError(`Failed to create follow up game. HTTP Error: ${response.status}`); return '';
console.error(response);
} }
} }
/** /**
* Submits the value of the username-input to set the username. * Submits the value of the username-input to set the username.
* @returns {Promise<void>} * @returns {Promise<Boolean>}
*/ */
async function submitUsername() { async function submitUsername() {
let unameInput = document.querySelector('#username-input'); let unameInput = document.querySelector('#input-username');
let username = unameInput.value.replace(/^\s+|\s+$/g, ''); let username = unameInput.value.replace(/^\s+|\s+$/g, '');
if (username.length > 1 && username !== 'anonymous') {
if (username.length > 1) {
let response = await postGraphqlQuery(` let response = await postGraphqlQuery(`
mutation($username:String!) { mutation($username:String!) {
bingo { bingo {
setUsername(input: {username: $username}) { setUsername(username: $username) {
id id
username username
} }
} }
}`, { }`, {username: username});
username: username
}, `/graphql?game=${getGameParam()}`);
if (response.status === 200) { if (response.status === 200) {
unameInput.value = ''; return true;
unameInput.placeholder = response.data.username;
document.querySelector('#username-form').remove();
document.querySelector('.greyover').remove();
} else { } else {
showError(`Failed to submit username. HTTP Error: ${response.status}`); showError(`Failed to submit username. HTTP Error: ${response.status}`);
console.error(response); console.error(response);
return false;
} }
} else { } else {
showError('You need to provide a username (minimum 2 characters)!'); showError('You need to provide a username (minimum 2 characters)!');
} return false;
}
/**
* toggles a word (toggle occures on response)
* @param word {String} - the base64 encoded bingo word
* @returns {Promise<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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 = `<span class="player-name-span">${player.username}</span>`;
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?');
} }
} }
@ -304,40 +73,16 @@ function showError(errorMessage) {
errorDiv.setAttribute('class', 'errorDiv'); errorDiv.setAttribute('class', 'errorDiv');
errorDiv.innerHTML = `<span>${errorMessage}</span>`; errorDiv.innerHTML = `<span>${errorMessage}</span>`;
let contCont = document.querySelector('#content-container'); let contCont = document.querySelector('#content-container');
if (contCont) if (contCont) {
contCont.appendChild(errorDiv); contCont.appendChild(errorDiv);
else } else {
alert(errorMessage); alert(errorMessage);
}
setTimeout(() => { setTimeout(() => {
errorDiv.remove(); errorDiv.remove();
}, 10000); }, 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 * Adds a message to the chat
* @param messageObject {Object} - the message object returned by graphql * @param messageObject {Object} - the message object returned by graphql
@ -346,55 +91,25 @@ function addChatMessage(messageObject) {
let msgSpan = document.createElement('span'); let msgSpan = document.createElement('span');
msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('class', 'chatMessage');
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.username}:</span>
<span class="chatMessageContent">${messageObject.htmlContent}</span>`; <span class="chatMessageContent">${messageObject.htmlContent}</span>`;
else } else {
msgSpan.innerHTML = ` msgSpan.innerHTML = `
<span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`; <span class="chatMessageContent ${messageObject.type}">${messageObject.htmlContent}</span>`;
}
let chatContent = document.querySelector('#chat-content'); let chatContent = document.querySelector('#chat-content');
chatContent.appendChild(msgSpan); chatContent.appendChild(msgSpan);
chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom 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();
}
/** window.addEventListener("unhandledrejection", function (promiseRejectionEvent) {
* 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) {
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 = () => { 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:`;
};
}; };

@ -91,3 +91,34 @@ function insertParam(key, value) {
kvp[kvp.length] = [key, value].join('='); kvp[kvp.length] = [key, value].join('=');
document.location.search = kvp.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<void>}
*/
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');
}
}

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

@ -41,10 +41,6 @@ textarea
#words-container #words-container
display: none !important display: none !important
#username-form
width: calc(100% - 2rem) !important
left: 0 !important
#hide-player-container-button #hide-player-container-button
display: none display: none
@ -138,32 +134,6 @@ textarea
#bingo-button #bingo-button
transition-duration: 0.8s 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 #content-container
display: grid display: grid
grid-template-columns: 25% 75% grid-template-columns: 25% 75%
@ -299,3 +269,30 @@ textarea
top: 0 top: 0
left: 0 left: 0
background-color: rgba(0, 0, 0, 0.5) 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%

@ -1,3 +1,6 @@
@import vars
@import animations
.tableRow .tableRow
display: table-row display: table-row
@ -17,3 +20,26 @@
.inline-grid .inline-grid
display: 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

@ -5,3 +5,9 @@
color: $primarySurface color: $primarySurface
border: 2px solid $primarySurface border: 2px solid $primarySurface
transition-duration: 0.2s 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

@ -4,3 +4,5 @@ $secondary: teal
$borderRadius: 20px $borderRadius: 20px
$inactive: #aaa $inactive: #aaa
$error: #a00 $error: #a00
$success: #0a0
$pending: #aa0

@ -1142,8 +1142,8 @@ router.use(async (req, res, next) => {
router.get('/', (req, res) => { router.get('/', (req, res) => {
let bingoUser = req.session.bingoUser; let bingoUser = req.session.bingoUser;
if (req.query.game) { if (req.query.g) {
let gameId = req.query.game || bingoUser.game; let lobbyId = req.query.g;
if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { if (bingoSessions[gameId] && !bingoSessions[gameId].finished) {
bingoUser.game = gameId; bingoUser.game = gameId;
@ -1163,7 +1163,7 @@ router.get('/', (req, res) => {
res.render('bingo/bingo-submit'); res.render('bingo/bingo-submit');
} }
} else { } else {
res.render('bingo/bingo-submit'); res.render('bingo/bingo-create');
} }
}); });
@ -1188,7 +1188,7 @@ router.graphqlResolver = async (req, res) => {
}, },
// mutations // mutations
setUsername: async ({username}) => { setUsername: async ({username}) => {
username = username.substring(0, 30); // only allow 30 characters username = replaceTagSigns(username.substring(0, 30)); // only allow 30 characters
if (!playerId) { if (!playerId) {
req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; req.session.bingoPlayerId = (await bdm.addPlayer(username)).id;
@ -1228,16 +1228,25 @@ router.graphqlResolver = async (req, res) => {
} }
}, },
kickPlayer: async ({pid}) => { kickPlayer: async ({pid}) => {
let admin = await lobbyWrapper.admin();
if (admin.id === playerId) {
let result = await bdm.removePlayerFromLobby(pid, lobbyId); let result = await bdm.removePlayerFromLobby(pid, lobbyId);
return new LobbyWrapper(result.id, result); return new LobbyWrapper(result.id, result);
}
}, },
startRound: async () => { startRound: async () => {
let admin = await lobbyWrapper.admin();
if (admin.id === playerId) {
await lobbyWrapper.startNewRound(); await lobbyWrapper.startNewRound();
return lobbyWrapper.currentRound(); return lobbyWrapper.currentRound();
}
}, },
setGridSize: async ({gridSize}) => { setGridSize: async ({gridSize}) => {
let admin = await lobbyWrapper.admin();
if (admin.id === playerId) {
await lobbyWrapper.setGridSize(gridSize); await lobbyWrapper.setGridSize(gridSize);
return lobbyWrapper; return lobbyWrapper;
}
}, },
setWords: async({words}) => { setWords: async({words}) => {
let admin = await lobbyWrapper.admin(); let admin = await lobbyWrapper.admin();

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