Implemented graphql in front and backend

- implementation of graphql api-point for bingo
- added player view
- added player view toggle button
-  added submit on enter
- improved mobile layout
pull/3/head
Trivernis 5 years ago
parent 1e96cbe68e
commit 02fd2665f2

@ -1,22 +1,22 @@
type BingoMutation { type BingoMutation {
# creates a game of bingo and returns the game id # creates a game of bingo and returns the game id
createGame(words: [String!]!, size: Int = 3): BingoGame createGame(input: CreateGameInput!): BingoGame
# submit a bingo to the active game session # submit a bingo to the active game session
submitBingo: Boolean submitBingo: BingoGame
# toggle a word (heared or not) on the sessions grid # toggle a word (heared or not) on the sessions grid
toggleWord(word: String, base64Word: String): BingoGrid toggleWord(input: WordInput!): BingoGrid
# set the username of the current session # set the username of the current session
setUsername(username: String!): BingoUser setUsername(input: UsernameInput): BingoUser
} }
type BingoQuery { type BingoQuery {
# Returns the currently active bingo game # Returns the currently active bingo game
gameInfo(id: ID): BingoGame gameInfo(input: IdInput): BingoGame
# If there is a bingo in the fields. # If there is a bingo in the fields.
checkBingo: Boolean checkBingo: Boolean
@ -25,6 +25,36 @@ type BingoQuery {
activeGrid: BingoGrid activeGrid: BingoGrid
} }
input CreateGameInput {
# the words used to fill the bingo grid
words: [String!]!
# the size of the bingo grid
size: Int! = 3
}
input WordInput {
# the normal word string
word: String
# the base64-encoded word
base64Word: String
}
input UsernameInput {
# the username string
username: String!
}
input IdInput {
# the id
id: ID!
}
type BingoGame { type BingoGame {
# the id of the bingo game # the id of the bingo game
@ -37,7 +67,7 @@ type BingoGame {
gridSize: Int gridSize: Int
# an array of players active in the bingo game # an array of players active in the bingo game
players(id: ID): [BingoUser] players(input: IdInput): [BingoUser]
# the player-ids that scored a bingo # the player-ids that scored a bingo
bingos: [String]! bingos: [String]!
@ -78,5 +108,6 @@ type BingoField {
# if the word was already heared # if the word was already heared
submitted: Boolean! submitted: Boolean!
# the base64 encoded word
base64Word: String base64Word: String
} }

@ -1,92 +1,233 @@
/**
* Returns the value of the url-param 'game'
* @returns {string}
*/
function getGameParam() {
return window.location.href.match(/\?game=(\w+)/)[1];
}
/**
* 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() { 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');
let size = document.querySelector('#bingo-grid-size').value; let size = document.querySelector('#bingo-grid-size').value;
let dimY = document.querySelector('#bingo-grid-y').value;
let response = await postLocData({
bingoWords: words,
size: size
});
let data = JSON.parse(response.data); let response = await postGraphqlQuery(`
let gameid = data.id; mutation($words:[String!]!, $size:Int!) {
insertParam('game', gameid); 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 {
console.error(response)
}
} }
/**
* Submits the value of the username-input to set the username.
* @returns {Promise<void>}
*/
async function submitUsername() { async function submitUsername() {
let unameInput = document.querySelector('#username-input'); let unameInput = document.querySelector('#username-input');
let username = unameInput.value; let username = unameInput.value;
let response = await postLocData({ let response = await postGraphqlQuery(`
mutation($username:String!) {
bingo {
setUsername(input: {username: $username}) {
id
username
}
}
}`, {
username: username username: username
}); },`/graphql?game=${getGameParam()}`);
unameInput.value = ''; if (response.status === 200) {
unameInput.placeholder = username; unameInput.value = '';
document.querySelector('#username-form').remove(); unameInput.placeholder = response.data.username;
document.querySelector('.greyover').remove(); document.querySelector('#username-form').remove();
document.querySelector('.greyover').remove();
console.log(response); } else {
console.error(response);
}
} }
/**
* toggles a word (toggle occures on response)
* @param word {String} - the base64 encoded bingo word
* @returns {Promise<void>}
*/
async function submitWord(word) { async function submitWord(word) {
let response = await postLocData({ let response = await postGraphqlQuery(`
bingoWord: word mutation($word:String!) {
}); bingo {
console.log(response); toggleWord(input: {base64Word: $word}) {
bingo
fieldGrid {
submitted
base64Word
}
}
}
}`, {
word: word
},`/graphql?game=${getGameParam()}`);
let data = JSON.parse(response.data); if (response.status === 200 && response.data.bingo.toggleWord) {
for (let row of data.fieldGrid) { let fieldGrid = response.data.bingo.toggleWord.fieldGrid;
for (let field of row) { for (let row of fieldGrid) {
document.querySelectorAll(`.bingo-word-panel[b-word="${field.base64Word}"]`).forEach(x => { for (let field of row) {
x.setAttribute('b-sub', field.submitted); 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');
} }
}
if (data.bingo) {
document.querySelector('#bingo-button').setAttribute('class', '');
} else { } else {
document.querySelector('#bingo-button').setAttribute('class', 'hidden'); 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() { async function submitBingo() {
let response = await postLocData({ let response = await postGraphqlQuery(`
bingo: true mutation {
}); bingo {
let data = JSON.parse(response.data); submitBingo {
if (data.bingos.length > 0) { id
displayWinner(data.users[data.bingos[0]].username); bingos
clearInterval(refrInterval) 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 {
console.error(response);
} }
console.log(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() { async function refresh() {
let response = await postLocData({}); let response = await postGraphqlQuery(`
if (response.status === 400) query {
clearInterval(refrInterval); bingo {
let data = JSON.parse(response.data); gameInfo {
if (data.bingos.length > 0) { id
displayWinner(data.users[data.bingos[0]].username); bingos
clearInterval(refrInterval) players {
username
id
}
}
}
}`, 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;
}
}
}
}
} else {
if (response.status === 400)
clearInterval(refrInterval);
console.error(response);
} }
console.log(response);
} }
/**
* Displays the winner of the game in a popup.
* @param name {String} - the name of the winner
*/
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'); let winnerSpan = document.createElement('span');
winnerDiv.setAttribute('class', 'popup'); winnerDiv.setAttribute('class', 'popup');
winnerDiv.setAttribute('style', 'cursor: pointer');
greyoverDiv.setAttribute('class', 'greyover'); greyoverDiv.setAttribute('class', 'greyover');
winnerSpan.innerText = `${name} has won!`; winnerSpan.innerText = `${name} has won!`;
winnerDiv.appendChild(winnerSpan); winnerDiv.appendChild(winnerSpan);
winnerDiv.onclick = () => {
window.location.reload();
};
document.body.append(greyoverDiv); document.body.append(greyoverDiv);
document.body.appendChild(winnerDiv); document.body.appendChild(winnerDiv);
} }
/**
* 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.onload = () => { window.onload = () => {
if (window && !document.querySelector('#bingoform')) { if (window && !document.querySelector('#bingoform')) {
refrInterval = setInterval(refresh, 1000); refrInterval = setInterval(refresh, 1000); // global variable to clear
} }
let gridSizeElem = document.querySelector('#bingo-grid-size'); let gridSizeElem = document.querySelector('#bingo-grid-size');
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;

@ -1,4 +1,4 @@
function postLocData(postBody) { function postData(url, postBody) {
let request = new XMLHttpRequest(); let request = new XMLHttpRequest();
return new Promise((res, rej) => { return new Promise((res, rej) => {
@ -13,12 +13,39 @@ function postLocData(postBody) {
rej(request.error); rej(request.error);
}; };
request.open('POST', '#', true); request.open('POST', url, true);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(postBody)); request.send(JSON.stringify(postBody));
}); });
} }
async function postLocData(postBody) {
return await postData('#', postBody);
}
async function postGraphqlQuery(query, variables, url) {
let body = {
query: query,
variables: variables
};
let response = await postData(url || '/graphql', body);
let resData = JSON.parse(response.data);
if (response.status === 200) {
return {
status: response.status,
data: resData.data,
};
} else {
return {
status: response.status,
data: resData.data,
errors: resData.errors,
requestBody: body
};
}
}
function insertParam(key, value) { function insertParam(key, value) {
key = encodeURI(key); key = encodeURI(key);
value = encodeURI(value); value = encodeURI(value);
@ -42,4 +69,4 @@ function insertParam(key, value) {
} }
document.location.search = kvp.join('&'); document.location.search = kvp.join('&');
} }

@ -18,6 +18,14 @@ textarea
#words-container #words-container
width: 100% width: 100%
height: 80% height: 80%
#content-container #row-1 #players-container div
display: none
padding: 0
#username-form
width: calc(100% - 2rem) !important
left: 0 !important
#hide-player-container-button
display: none
@media(min-device-width: 641px) @media(min-device-width: 641px)
textarea textarea
@ -25,7 +33,7 @@ textarea
width: 50% width: 50%
#words-container #words-container
width: 100% width: 100%
height: 88% height: 100%
.number-input .number-input
width: 4rem width: 4rem
@ -96,14 +104,36 @@ textarea
button button
cursor: pointer cursor: pointer
width: 100%
margin: 1rem auto
input[type='text'] input[type='text']
cursor: text cursor: text
width: 100%
#username-form * #username-form *
display: inline-block display: inline-block
vertical-align: middle vertical-align: middle
#content-container
display: table
height: 100%
width: 100%
#row-1
display: table-row
height: 85%
#players-container
display: table-cell
padding: 0.5rem
transition-duration: 1s
.player-container
@include default-element
padding: 0.5rem
max-width: 14rem
.popup .popup
@include default-element @include default-element
height: 5% height: 5%

@ -2,7 +2,7 @@
display: table-row display: table-row
.hidden .hidden
display: None display: None !important
.popup .popup
height: 60% height: 60%
@ -10,4 +10,4 @@
z-index: 1000 z-index: 1000
position: fixed position: fixed
top: 20% top: 20%
left: 30% left: 30%

@ -37,8 +37,9 @@ class BingoSession {
* @returns {any[]|*} * @returns {any[]|*}
*/ */
players(args) { players(args) {
if (args.id) let input = args? args.input : null;
return [this.users[args.id]]; if (input && input.id)
return [this.users[input.id]];
else else
return Object.values(this.users); return Object.values(this.users);
} }
@ -224,7 +225,7 @@ router.use((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.game) {
let gameId = req.query.game; let gameId = req.query.game || bingoUser.game;
if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { if (bingoSessions[gameId] && !bingoSessions[gameId].finished) {
bingoUser.game = gameId; bingoUser.game = gameId;
@ -234,7 +235,11 @@ router.get('/', (req, res) => {
if (!bingoUser.grids[gameId]) { if (!bingoUser.grids[gameId]) {
bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words); bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words);
} }
res.render('bingo/bingo-game', {grid: bingoUser.grids[gameId].fieldGrid, username: bingoUser.username}); res.render('bingo/bingo-game', {
grid: bingoUser.grids[gameId].fieldGrid,
username: bingoUser.username,
players: bingoSession.players()
});
} else { } else {
res.render('bingo/bingo-submit'); res.render('bingo/bingo-submit');
} }
@ -243,81 +248,28 @@ router.get('/', (req, res) => {
} }
}); });
router.post('/', (req, res) => {
let data = req.body;
let gameId = req.query.game;
let bingoUser = req.session.bingoUser;
let bingoSession = bingoSessions[gameId];
if (data.bingoWords) {
let words = data.bingoWords;
let size = data.size;
let game = new BingoSession(words, size);
bingoSessions[game.id] = game;
setTimeout(() => { // delete the game after one day
delete bingoSessions[game.id];
}, 86400000);
res.send(game);
} else if (data.username) {
bingoUser.username = data.username;
bingoSessions[gameId].addUser(bingoUser);
res.send(bingoUser);
} else if (data.game) {
res.send(bingoSessions[data.game]);
} else if (data.bingoWord) {
console.log(typeof bingoUser.grids[gameId]);
if (bingoUser.grids[gameId])
toggleHeared(data.bingoWord, bingoUser.grids[gameId]);
res.send(bingoUser.grids[gameId]);
} else if (data.bingo) {
if (checkBingo(bingoUser.grids[gameId])) {
if (!bingoSession.bingos.includes(bingoUser.id))
bingoSession.bingos.push(bingoUser.id);
bingoSession.finished = true;
setTimeout(() => { // delete the finished game after five minutes
delete bingoSessions[gameId];
}, 360000);
res.send(bingoSession);
} else {
res.status(400);
res.send({'error': "this is not a bingo!"})
}
} else if (bingoSession) {
res.send(bingoSession);
} else {
res.status(400);
res.send({
error: 'invalid request data'
})
}
});
router.graphqlResolver = (req) => { router.graphqlResolver = (req) => {
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];
return { return {
// queries // queries
gameInfo: (args) => { gameInfo: ({input}) => {
if (args.id) if (input && input.id)
return bingoSessions[args.id]; return bingoSessions[input.id];
else else
return bingoSession; return bingoSession;
}, },
checkBingo: (args) => { checkBingo: () => {
return checkBingo(bingoUser.grids[gameId]) return checkBingo(bingoUser.grids[gameId])
}, },
activeGrid: (args) => { activeGrid: () => {
return bingoUser.grids[gameId]; return bingoUser.grids[gameId];
}, },
// mutation // mutation
createGame: (args) => { createGame: ({input}) => {
let words = args.words; let words = input.words;
let size = args.size; let size = input.size;
let game = new BingoSession(words, size); let game = new BingoSession(words, size);
bingoSessions[game.id] = game; bingoSessions[game.id] = game;
@ -328,31 +280,33 @@ router.graphqlResolver = (req) => {
return game; return game;
}, },
submitBingo: (args) => { submitBingo: () => {
if (checkBingo(bingoUser.grids[gameId])) { if (checkBingo(bingoUser.grids[gameId])) {
if (!bingoSession.bingos.includes(bingoUser.id)) if (!bingoSession.bingos.includes(bingoUser.id))
bingoSession.bingos.push(bingoUser.id); bingoSession.bingos.push(bingoUser.id);
bingoSession.finished = true; bingoSession.finished = true;
setTimeout(() => { // delete the finished game after five minutes setTimeout(() => { // delete the finished game after five minutes
delete bingoSessions[gameId]; delete bingoSessions[gameId];
}, 360000); }, 300000);
return true; return bingoSession;
} else { } else {
return false; return bingoSession;
} }
}, },
toggleWord: (args) => { toggleWord: ({input}) => {
if (args.word || args.base64Word) { if (input.word || input.base64Word) {
args.base64Word = args.base64Word || Buffer.from(args.word).toString('base-64'); input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64');
if (bingoUser.grids[gameId]) if (bingoUser.grids[gameId])
toggleHeared(args.base64Word, bingoUser.grids[gameId]); toggleHeared(input.base64Word, bingoUser.grids[gameId]);
return bingoUser.grids[gameId]; return bingoUser.grids[gameId];
} }
}, },
setUsername: (args) => { setUsername: ({input}) => {
if (args.username) { if (input.username) {
bingoUser.username = args.username; bingoUser.username = input.username;
bingoSession.addUser(bingoUser);
if (bingoSession)
bingoSession.addUser(bingoUser);
return bingoUser; return bingoUser;
} }

@ -3,13 +3,21 @@ include bingo-layout
block content block content
if username === 'anonymous' if username === 'anonymous'
div(class='greyover') div(class='greyover')
div(id='username-form') div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)')
input(type='text', id='username-input', placeholder=username) input(type='text', id='username-input', placeholder=username)
button(onclick='submitUsername()') Set Username button(onclick='submitUsername()') Set Username
div(id='words-container') div(id='content-container')
each val in grid button(id='hide-player-container-button', onclick='togglePlayerContainer()') Toggle Player View
div(class='bingo-word-row') div(id='row-1')
each field in val div(id='players-container')
div(class='bingo-word-panel', onclick=`submitWord('${field.base64Word}')`, b-word=field.base64Word, b-sub='false') each player in players
span= field.word div(class='player-container', b-pid=`${player.id}`)
button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! 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!

@ -4,7 +4,7 @@ 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) input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8, 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')

Loading…
Cancel
Save