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 6 years ago
parent 1e96cbe68e
commit 02fd2665f2

@ -1,22 +1,22 @@
type BingoMutation {
# 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
submitBingo: Boolean
submitBingo: BingoGame
# 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
setUsername(username: String!): BingoUser
setUsername(input: UsernameInput): BingoUser
}
type BingoQuery {
# Returns the currently active bingo game
gameInfo(id: ID): BingoGame
gameInfo(input: IdInput): BingoGame
# If there is a bingo in the fields.
checkBingo: Boolean
@ -25,6 +25,36 @@ type BingoQuery {
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 {
# the id of the bingo game
@ -37,7 +67,7 @@ type BingoGame {
gridSize: Int
# an array of players active in the bingo game
players(id: ID): [BingoUser]
players(input: IdInput): [BingoUser]
# the player-ids that scored a bingo
bingos: [String]!
@ -78,5 +108,6 @@ type BingoField {
# if the word was already heared
submitted: Boolean!
# the base64 encoded word
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() {
let textContent = document.querySelector('#bingo-textarea').value;
let words = textContent.replace(/[<>]/g, '').split('\n');
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 gameid = data.id;
insertParam('game', gameid);
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 {
console.error(response)
}
}
/**
* Submits the value of the username-input to set the username.
* @returns {Promise<void>}
*/
async function submitUsername() {
let unameInput = document.querySelector('#username-input');
let username = unameInput.value;
let response = await postLocData({
let response = await postGraphqlQuery(`
mutation($username:String!) {
bingo {
setUsername(input: {username: $username}) {
id
username
}
}
}`, {
username: username
});
unameInput.value = '';
unameInput.placeholder = username;
document.querySelector('#username-form').remove();
document.querySelector('.greyover').remove();
console.log(response);
},`/graphql?game=${getGameParam()}`);
if (response.status === 200) {
unameInput.value = '';
unameInput.placeholder = response.data.username;
document.querySelector('#username-form').remove();
document.querySelector('.greyover').remove();
} 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) {
let response = await postLocData({
bingoWord: word
});
console.log(response);
let response = await postGraphqlQuery(`
mutation($word:String!) {
bingo {
toggleWord(input: {base64Word: $word}) {
bingo
fieldGrid {
submitted
base64Word
}
}
}
}`, {
word: word
},`/graphql?game=${getGameParam()}`);
let data = JSON.parse(response.data);
for (let row of data.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.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');
}
}
if (data.bingo) {
document.querySelector('#bingo-button').setAttribute('class', '');
} 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() {
let response = await postLocData({
bingo: true
});
let data = JSON.parse(response.data);
if (data.bingos.length > 0) {
displayWinner(data.users[data.bingos[0]].username);
clearInterval(refrInterval)
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 {
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() {
let response = await postLocData({});
if (response.status === 400)
clearInterval(refrInterval);
let data = JSON.parse(response.data);
if (data.bingos.length > 0) {
displayWinner(data.users[data.bingos[0]].username);
clearInterval(refrInterval)
let response = await postGraphqlQuery(`
query {
bingo {
gameInfo {
id
bingos
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) {
let winnerDiv = document.createElement('div');
let greyoverDiv = document.createElement('div');
let winnerSpan = document.createElement('span');
winnerDiv.setAttribute('class', 'popup');
winnerDiv.setAttribute('style', 'cursor: pointer');
greyoverDiv.setAttribute('class', 'greyover');
winnerSpan.innerText = `${name} has won!`;
winnerDiv.appendChild(winnerSpan);
winnerDiv.onclick = () => {
window.location.reload();
};
document.body.append(greyoverDiv);
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 = () => {
if (window && !document.querySelector('#bingoform')) {
refrInterval = setInterval(refresh, 1000);
refrInterval = setInterval(refresh, 1000); // global variable to clear
}
let gridSizeElem = document.querySelector('#bingo-grid-size');
document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value;

@ -1,4 +1,4 @@
function postLocData(postBody) {
function postData(url, postBody) {
let request = new XMLHttpRequest();
return new Promise((res, rej) => {
@ -13,12 +13,39 @@ function postLocData(postBody) {
rej(request.error);
};
request.open('POST', '#', true);
request.open('POST', url, true);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
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) {
key = encodeURI(key);
value = encodeURI(value);
@ -42,4 +69,4 @@ function insertParam(key, value) {
}
document.location.search = kvp.join('&');
}
}

@ -18,6 +18,14 @@ textarea
#words-container
width: 100%
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)
textarea
@ -25,7 +33,7 @@ textarea
width: 50%
#words-container
width: 100%
height: 88%
height: 100%
.number-input
width: 4rem
@ -96,14 +104,36 @@ textarea
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
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
@include default-element
height: 5%

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

@ -37,8 +37,9 @@ class BingoSession {
* @returns {any[]|*}
*/
players(args) {
if (args.id)
return [this.users[args.id]];
let input = args? args.input : null;
if (input && input.id)
return [this.users[input.id]];
else
return Object.values(this.users);
}
@ -224,7 +225,7 @@ router.use((req, res, next) => {
router.get('/', (req, res) => {
let bingoUser = req.session.bingoUser;
if (req.query.game) {
let gameId = req.query.game;
let gameId = req.query.game || bingoUser.game;
if (bingoSessions[gameId] && !bingoSessions[gameId].finished) {
bingoUser.game = gameId;
@ -234,7 +235,11 @@ router.get('/', (req, res) => {
if (!bingoUser.grids[gameId]) {
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 {
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) => {
let bingoUser = req.session.bingoUser || new BingoUser();
let gameId = req.query.game || bingoUser.game || null;
let bingoSession = bingoSessions[gameId];
return {
// queries
gameInfo: (args) => {
if (args.id)
return bingoSessions[args.id];
gameInfo: ({input}) => {
if (input && input.id)
return bingoSessions[input.id];
else
return bingoSession;
},
checkBingo: (args) => {
checkBingo: () => {
return checkBingo(bingoUser.grids[gameId])
},
activeGrid: (args) => {
activeGrid: () => {
return bingoUser.grids[gameId];
},
// mutation
createGame: (args) => {
let words = args.words;
let size = args.size;
createGame: ({input}) => {
let words = input.words;
let size = input.size;
let game = new BingoSession(words, size);
bingoSessions[game.id] = game;
@ -328,31 +280,33 @@ router.graphqlResolver = (req) => {
return game;
},
submitBingo: (args) => {
submitBingo: () => {
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);
return true;
}, 300000);
return bingoSession;
} else {
return false;
return bingoSession;
}
},
toggleWord: (args) => {
if (args.word || args.base64Word) {
args.base64Word = args.base64Word || Buffer.from(args.word).toString('base-64');
toggleWord: ({input}) => {
if (input.word || input.base64Word) {
input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64');
if (bingoUser.grids[gameId])
toggleHeared(args.base64Word, bingoUser.grids[gameId]);
toggleHeared(input.base64Word, bingoUser.grids[gameId]);
return bingoUser.grids[gameId];
}
},
setUsername: (args) => {
if (args.username) {
bingoUser.username = args.username;
bingoSession.addUser(bingoUser);
setUsername: ({input}) => {
if (input.username) {
bingoUser.username = input.username;
if (bingoSession)
bingoSession.addUser(bingoUser);
return bingoUser;
}

@ -3,13 +3,21 @@ include bingo-layout
block content
if username === 'anonymous'
div(class='greyover')
div(id='username-form')
div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)')
input(type='text', id='username-input', placeholder=username)
button(onclick='submitUsername()') Set 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='false')
span= field.word
button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo!
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='button-container')
button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo!

@ -4,7 +4,7 @@ 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)
input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8, onkeypress='submitOnEnter(event, submitBingoWords)')
span x
span(id='bingo-grid-y', class='number-input') 3
div(class='stretchDiv')

Loading…
Cancel
Save