Redesign and Rematch

- added the option to leave or play again after a match
- added error messages
- redesigned with css-grids
pull/3/head
Trivernis 5 years ago
parent 68faea7d86
commit 4d87c70b69

@ -21,10 +21,10 @@ let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml'));
if (fsx.existsSync('config.yaml')) if (fsx.existsSync('config.yaml'))
Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml')));
let graphqlResolver = (request) => { let graphqlResolver = (request, response) => {
return { return {
time: Date.now(), time: Date.now(),
bingo: bingoRouter.graphqlResolver(request) bingo: bingoRouter.graphqlResolver(request, response)
} }
}; };
let app = express(); let app = express();
@ -58,10 +58,10 @@ app.use('/', indexRouter);
app.use('/users', usersRouter); app.use('/users', usersRouter);
app.use(/\/riddle(\/.*)?/, riddleRouter); app.use(/\/riddle(\/.*)?/, riddleRouter);
app.use('/bingo', bingoRouter); app.use('/bingo', bingoRouter);
app.use('/graphql', graphqlHTTP(request => { app.use('/graphql', graphqlHTTP((request, response) => {
return { return {
schema: buildSchema(importSchema('./graphql/schema.graphql')), schema: buildSchema(importSchema('./graphql/schema.graphql')),
rootValue: graphqlResolver(request), rootValue: graphqlResolver(request, response),
context: {session: request.session}, context: {session: request.session},
graphiql: true graphiql: true
}; };

@ -11,6 +11,9 @@ type BingoMutation {
# set the username of the current session # set the username of the current session
setUsername(input: UsernameInput): BingoUser setUsername(input: UsernameInput): BingoUser
# recreates the active game to a follow-up
createFollowupGame: BingoGame
} }
type BingoQuery { type BingoQuery {
@ -74,6 +77,9 @@ type BingoGame {
# if the game has already finished # if the game has already finished
finished: Boolean finished: Boolean
# the id of the followup game if it has been created
followup: ID
} }
type BingoUser { type BingoUser {

@ -10,45 +10,63 @@ function getGameParam() {
return ''; 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 * Submits the bingo words to create a game
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function submitBingoWords() { async function submitBingoWords() {
let textContent = document.querySelector('#bingo-textarea').value; let textContent = document.querySelector('#bingo-textarea').value;
let words = textContent.replace(/[<>]/g, '').split('\n'); let words = textContent.replace(/[<>]/g, '').split('\n').filter((el) => {
let size = document.querySelector('#bingo-grid-size').value; 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(` let response = await postGraphqlQuery(`
mutation($words:[String!]!, $size:Int!) { mutation {
bingo { bingo {
createGame(input: { createFollowupGame {
words: $words,
size: $size
}) {
id id
} }
} }
}`, { }`,null,`/graphql?game=${getGameParam()}`);
words: words, if (response.status === 200 && response.data.bingo.createFollowupGame) {
size: Number(size) let gameid = response.data.bingo.createFollowupGame.id;
}, `/graphql?game=${getGameParam()}`);
if (response.status === 200) {
let gameid = response.data.bingo.createGame.id;
insertParam('game', gameid); insertParam('game', gameid);
} else { } 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('#username-form').remove();
document.querySelector('.greyover').remove(); document.querySelector('.greyover').remove();
} else { } else {
showError(`Failed to submit username. HTTP Error: ${response.status}`);
console.error(response); console.error(response);
} }
} }
@ -116,6 +135,7 @@ async function submitWord(word) {
document.querySelector('#bingo-button').setAttribute('class', 'hidden'); document.querySelector('#bingo-button').setAttribute('class', 'hidden');
} }
} else { } else {
showError(`Failed to submit word. HTTP Error: ${response.status}`);
console.error(response); console.error(response);
} }
} }
@ -146,6 +166,7 @@ async function submitBingo() {
clearInterval(refrInterval) clearInterval(refrInterval)
} }
} else { } else {
showError(`Failed to submit Bingo. HTTP Error: ${response.status}`);
console.error(response); console.error(response);
} }
} }
@ -196,6 +217,7 @@ async function refresh() {
if (response.status === 400) if (response.status === 400)
clearInterval(refrInterval); clearInterval(refrInterval);
console.error(response); console.error(response);
showError('No session found. Are cookies allowed?');
} }
} }
@ -206,19 +228,38 @@ async function refresh() {
function displayWinner(name) { function displayWinner(name) {
let winnerDiv = document.createElement('div'); let winnerDiv = document.createElement('div');
let greyoverDiv = document.createElement('div'); let greyoverDiv = document.createElement('div');
let winnerSpan = document.createElement('span');
winnerDiv.setAttribute('class', 'popup'); winnerDiv.setAttribute('class', 'popup');
winnerDiv.setAttribute('style', 'cursor: pointer'); winnerDiv.innerHTML = `
<h1>${name} has won!</h1>
<button id="btn-again" onclick="createFollowup()">Again!</button>
<button id="btn-leave" onclick="window.location.reload()">Leave</button>
`;
greyoverDiv.setAttribute('class', 'greyover'); greyoverDiv.setAttribute('class', 'greyover');
winnerSpan.innerText = `${name} has won!`; //winnerDiv.onclick = () => {
winnerDiv.appendChild(winnerSpan); // window.location.reload();
winnerDiv.onclick = () => { //};
window.location.reload();
};
document.body.append(greyoverDiv); document.body.append(greyoverDiv);
document.body.appendChild(winnerDiv); document.body.appendChild(winnerDiv);
} }
/**
* Shows an error Message.
* @param errorMessage
*/
function showError(errorMessage) {
let errorDiv = document.createElement('div');
errorDiv.setAttribute('class', 'errorDiv');
errorDiv.innerHTML = `<span>${errorMessage}</span>`;
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 * Executes the provided function if the key-event is an ENTER-key
* @param event {Event} - the generated key event * @param event {Event} - the generated key event
@ -229,6 +270,11 @@ function submitOnEnter(event, func) {
func(); func();
} }
window.addEventListener("unhandledrejection", function(promiseRejectionEvent) {
promiseRejectionEvent.promise.catch(err => console.log(err));
showError('Connection problems... Is the server down?');
});
window.onload = () => { window.onload = () => {
if (window && !document.querySelector('#bingoform')) { if (window && !document.querySelector('#bingoform')) {
refrInterval = setInterval(refresh, 1000); // global variable to clear refrInterval = setInterval(refresh, 1000); // global variable to clear
@ -237,5 +283,6 @@ window.onload = () => {
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;
gridSizeElem.oninput = () => { gridSizeElem.oninput = () => {
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;
document.querySelector('#word-count').innerText = `Please provide at least ${gridSizeElem.value**2} phrases:`;
}; };
}; };

@ -1,15 +1,16 @@
@import ../mixins @import ../mixins
@import ../vars @import ../vars
button
margin: 1rem
textarea textarea
@include default-element @include default-element
display: block display: block
margin: 1rem margin: 1rem
border-radius: 0 border-radius: 0
font-size: 0.8em font-size: 0.8em
background-color: lighten($primary, 15%)
#word-count
margin: 1rem
@media(max-device-width: 641px) @media(max-device-width: 641px)
textarea textarea
@ -18,14 +19,20 @@ textarea
#words-container #words-container
width: 100% width: 100%
height: 80% height: 80%
#content-container #row-1 #players-container div #content-container
display: none grid-template-columns: 0 100% !important
padding: 0 grid-template-rows: 10% 80% 10% !important
#players-container div
display: none
padding: 0
#username-form #username-form
width: calc(100% - 2rem) !important width: calc(100% - 2rem) !important
left: 0 !important left: 0 !important
#hide-player-container-button #hide-player-container-button
display: none display: none
.popup
width: calc(100% - 2rem) !important
left: 0 !important
@media(min-device-width: 641px) @media(min-device-width: 641px)
textarea textarea
@ -70,11 +77,13 @@ textarea
border-collapse: collapse border-collapse: collapse
text-align: center text-align: center
vertical-align: middle vertical-align: middle
user-select: none
span span
vertical-align: middle vertical-align: middle
display: inline-block display: inline-block
word-break: break-word word-break: break-word
user-select: none
.bingo-word-panel:hover .bingo-word-panel:hover
background-color: darken($primary, 2%) background-color: darken($primary, 2%)
@ -116,35 +125,83 @@ textarea
vertical-align: middle vertical-align: middle
#content-container #content-container
display: table display: grid
grid-template-columns: 20% 80%
grid-template-rows: 10% 80% 10%
height: 100% height: 100%
width: 100% width: 100%
#row-1 #button-container
display: table-row grid-column-start: 1
height: 85% 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 display: table-cell
padding: 0.5rem font-size: 1.8rem
transition-duration: 1s vertical-align: middle
.player-container .player-container
@include default-element @include default-element
padding: 0.5rem padding: 0.5rem
max-width: 14rem margin: 0 0 1rem 0
max-width: 14rem
border-radius: 0
color: $primarySurface
border: 1px solid $inactive
.popup .popup
@include default-element @include default-element
height: 5% position: fixed
width: 40% display: grid
top: 47.5% height: calc(50% - 1rem)
width: calc(40% - 1rem)
top: 25%
left: 30% left: 30%
text-align: center text-align: center
transition-duration: 1s vertical-align: middle
span padding: 1rem
margin: 2% z-index: 1000
display: block
button
margin: 1rem
font-size: 2rem
.greyover .greyover
width: 100% width: 100%
@ -153,4 +210,4 @@ textarea
z-index: 99 z-index: 99
top: 0 top: 0
left: 0 left: 0
background-color: transparentize($primary, 0.5) background-color: rgba(0,0,0,0.5)

@ -11,3 +11,9 @@
position: fixed position: fixed
top: 20% top: 20%
left: 30% left: 30%
.grid
display: grid
.inline-grid
display: inline-grid

@ -36,16 +36,20 @@ button
font-size: 1.2rem font-size: 1.2rem
padding: 0.7rem padding: 0.7rem
transition-duration: 0.2s transition-duration: 0.2s
background-color: $secondary
button:hover button:hover
background-color: darken($primary, 2%) background-color: darken($secondary, 2%)
cursor: pointer cursor: pointer
button:active button:active
background-color: lighten($primary, 15%) background-color: lighten($secondary, 15%)
input input
@include default-element @include default-element
font-size: 1.2rem font-size: 1.2rem
background-color: lighten($primary, 10%) background-color: lighten($primary, 15%)
padding: 0.7rem padding: 0.7rem
textarea
background-color: lighten($primary, 15%)

@ -1,4 +1,6 @@
$primary: #223 $primary: #223
$primarySurface: white $primarySurface: white
$secondary: teal
$borderRadius: 20px $borderRadius: 20px
$inactive: #aaa
$error: #a00

@ -20,6 +20,7 @@ class BingoSession {
this.users = {}; this.users = {};
this.bingos = []; // array with the users that already had bingo this.bingos = []; // array with the users that already had bingo
this.finished = false; this.finished = false;
this.followup = null;
} }
/** /**
@ -43,6 +44,17 @@ class BingoSession {
else else
return Object.values(this.users); 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 { class BingoUser {
@ -109,7 +121,7 @@ function shuffleArray(array) {
*/ */
function inflateArray(array, minSize) { function inflateArray(array, minSize) {
let resultArray = array; 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++) for (let i = 0; i < iterations; i++)
resultArray = [...resultArray, ...resultArray]; resultArray = [...resultArray, ...resultArray];
return 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 bingoUser = req.session.bingoUser || new BingoUser();
let gameId = req.query.game || bingoUser.game || null; let gameId = req.query.game || bingoUser.game || null;
let bingoSession = bingoSessions[gameId]; let bingoSession = bingoSessions[gameId];
@ -268,17 +280,23 @@ router.graphqlResolver = (req) => {
}, },
// mutation // mutation
createGame: ({input}) => { 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 size = input.size;
let game = new BingoSession(words, size); if (words.length > 0 && size < 10 && size > 0) {
let game = new BingoSession(words, size);
bingoSessions[game.id] = game;
setTimeout(() => { // delete the game after one day bingoSessions[game.id] = game;
delete bingoSessions[game.id];
}, 86400000);
return game; setTimeout(() => { // delete the game after one day
delete bingoSessions[game.id];
}, 86400000);
return game;
} else {
res.status(400);
return null;
}
}, },
submitBingo: () => { submitBingo: () => {
if (checkBingo(bingoUser.grids[gameId])) { if (checkBingo(bingoUser.grids[gameId])) {
@ -296,20 +314,35 @@ router.graphqlResolver = (req) => {
toggleWord: ({input}) => { toggleWord: ({input}) => {
if (input.word || input.base64Word) { if (input.word || input.base64Word) {
input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); 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]); toggleHeared(input.base64Word, bingoUser.grids[gameId]);
return bingoUser.grids[gameId]; return bingoUser.grids[gameId];
} else {
res.status(400);
}
} else {
res.status(400);
} }
}, },
setUsername: ({input}) => { setUsername: ({input}) => {
if (input.username) { if (input.username) {
bingoUser.username = input.username; bingoUser.username = input.username.substring(0, 30); // only allow 30 characters
if (bingoSession) if (bingoSession)
bingoSession.addUser(bingoUser); bingoSession.addUser(bingoUser);
return bingoUser; return bingoUser;
} }
},
createFollowupGame: () => {
if (bingoSession) {
if (!bingoSession.followup)
return bingoSession.createFollowup();
else
return bingoSessions[bingoSession.followup];
} else {
res.status(400);
}
} }
}; };
}; };

@ -5,19 +5,19 @@ block content
div(class='greyover') div(class='greyover')
div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)') div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)')
input(type='text', id='username-input', placeholder=username) input(type='text', id='username-input', placeholder=username)
span Maximum is 30 characters.
button(onclick='submitUsername()') Set Username button(onclick='submitUsername()') Set Username
div(id='content-container') div(id='content-container')
button(id='hide-player-container-button', onclick='togglePlayerContainer()') Toggle Player View div(id='players-container')
div(id='row-1') h1 Players
div(id='players-container') each player in players
each player in players div(class='player-container', b-pid=`${player.id}`)
div(class='player-container', b-pid=`${player.id}`) span(class='player-name-span')= player.username
span(class='player-name-span')= player.username div(id='words-container')
div(id='words-container') each val in grid
each val in grid div(class='bingo-word-row')
div(class='bingo-word-row') each field in val
each field in val div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub=`${field.submitted}`)
div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub=`${field.submitted}`) span= field.word
span= field.word
div(id='button-container') div(id='button-container')
button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo!

@ -1,6 +1,7 @@
html html
head head
include ../includes/head include ../includes/head
title Bingo by Trivernis
script(type='text/javascript', src='/javascripts/bingo-web.js') script(type='text/javascript', src='/javascripts/bingo-web.js')
link(rel='stylesheet', href='/sass/bingo/style.sass') link(rel='stylesheet', href='/sass/bingo/style.sass')
body body

@ -4,9 +4,10 @@ block content
div(id='bingoform') div(id='bingoform')
div(id='bingoheader') div(id='bingoheader')
div 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 x
span(id='bingo-grid-y', class='number-input') 3 span(id='bingo-grid-y', class='number-input') 3
div(class='stretchDiv') div(class='stretchDiv')
button(onclick='submitBingoWords()') Submit button(onclick='submitBingoWords()') Submit
span(id='word-count') Please provide at least 9 phrases:
textarea(id='bingo-textarea', placeholder='Bingo Words') textarea(id='bingo-textarea', placeholder='Bingo Words')

@ -3,3 +3,4 @@ extends layout
block content block content
h1= title h1= title
p Welcome to #{title} p Welcome to #{title}
button(onclick='window.location.href="/bingo"') Bingo

@ -2,6 +2,6 @@ doctype html
html html
head head
title= title title= title
link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='/sass/style.sass')
body body
block content block content

Loading…
Cancel
Save