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 6 years ago
parent 68faea7d86
commit 4d87c70b69

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

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

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

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

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

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

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

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

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

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

@ -4,9 +4,10 @@ block content
div(id='bingoform')
div(id='bingoheader')
div
input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8, onkeypress='submitOnEnter(event, submitBingoWords)')
input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=7, onkeypress='submitOnEnter(event, submitBingoWords)')
span x
span(id='bingo-grid-y', class='number-input') 3
div(class='stretchDiv')
button(onclick='submitBingoWords()') Submit
span(id='word-count') Please provide at least 9 phrases:
textarea(id='bingo-textarea', placeholder='Bingo Words')

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

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

Loading…
Cancel
Save