Changes to style

- added backend limits
- added frontend limits
- added bingo statusbar
- improved bingo stylesheet
- added redirect on missing player
pull/15/head
Trivernis 5 years ago
parent d566505c3f
commit 6943719c7c

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- bingo lobbys
- kick function for bingo
- grid size input
- bingo status bar
## Changed
@ -33,11 +34,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `bin/www` now calls the init function of `app.js`
- graphql bingo api
- bingo frontend
- moved some bingo pug files to ./bingo/includes/
### Removed
- sqlite3 sesssion storage
- old frontend
- old bingo pug files
### Fixed

@ -59,7 +59,7 @@ async function init() {
app.use('/sass', compileSass({
root: './public/stylesheets/sass',
sourceMap: true,
watchFiles: false, // TODO: set true
watchFiles: true,
logToConsole: true
}));
app.use(express.static(path.join(__dirname, 'public')));

@ -11,3 +11,7 @@ Goblin Slayer
useless Aqua
theP0wner79
Pr0wn
Cool User 68
My name Jeff
Bingo Bingo Duolingo
Max Mustermann

@ -55,6 +55,14 @@ async function submitUsername() {
}
}
/**
* TODO: real join logic
*/
async function joinLobby() {
await submitUsername();
window.location.reload();
}
/**
* Creates a lobby and redirects to the lobby.
* @returns {Promise<boolean>}
@ -136,7 +144,16 @@ async function sendChatMessage() {
if (messageInput.value && messageInput.value.length > 0) {
let message = messageInput.value;
messageInput.value = '';
let response = await postGraphqlQuery(`
if (message === '/hideinfo' || message === '/showinfo') {
let jsStyle = document.querySelector('#js-style');
if (message === '/hideinfo')
jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {display: none}';
else
jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {}';
let chatContent = document.querySelector('#chat-content');
chatContent.scrollTop = chatContent.scrollHeight;
} else {
let response = await postGraphqlQuery(`
mutation($lobbyId:ID!, $message:String!){
bingo {
mutateLobby(id:$lobbyId) {
@ -151,12 +168,13 @@ async function sendChatMessage() {
}
}
}`, {message: message, lobbyId: getLobbyParam()});
if (response.status === 200) {
addChatMessage(response.data.bingo.mutateLobby.sendMessage);
} else {
messageInput.value = message;
console.error(response);
showError('Error when sending message.');
if (response.status === 200) {
addChatMessage(response.data.bingo.mutateLobby.sendMessage);
} else {
messageInput.value = message;
console.error(response);
showError('Error when sending message.');
}
}
}
}
@ -195,7 +213,7 @@ async function setLobbySettings(words, gridSize) {
/**
* Starts a new round of bingo
* @returns {Promise<void>}
* @returns {Promise<boolean>}
*/
async function startRound() {
let textinput = document.querySelector('#input-bingo-words');
@ -242,6 +260,8 @@ function getLobbyWords() {
async function submitFieldToggle(wordPanel) {
let row = Number(wordPanel.getAttribute('b-row'));
let column = Number(wordPanel.getAttribute('b-column'));
let wordClass = wordPanel.getAttribute('class');
wordPanel.setAttribute('class', wordClass + ' pending');
let response = await postGraphqlQuery(`
mutation($lobbyId:ID!, $row:Int!, $column:Int!){
bingo {
@ -255,6 +275,7 @@ async function submitFieldToggle(wordPanel) {
}
}
}`, {lobbyId: getLobbyParam(), row: row, column: column});
wordPanel.setAttribute('class', wordClass);
if (response.status === 200) {
wordPanel.setAttribute('b-sub', response.data.bingo.mutateLobby.toggleGridField.submitted);
@ -325,7 +346,32 @@ function displayWinner(roundInfo) {
* @param errorMessage
*/
function showError(errorMessage) {
// TODO: Implement
let errorContainer = document.querySelector('#error-message');
let indicator = document.querySelector('#status-indicator');
indicator.setAttribute('status', 'error');
errorContainer.innerText = errorMessage;
setTimeout(() => {
errorContainer.innerText = '';
indicator.setAttribute('status', 'idle');
}, 5000);
}
/**
* Wraps a function in a status report to display the status
* @param func
*/
async function statusWrap(func) {
let indicator = document.querySelector('#status-indicator');
indicator.setAttribute('status', 'pending');
try {
await func();
indicator.setAttribute('status', 'success');
setTimeout(() => {
indicator.setAttribute('status', 'idle');
}, 1000);
} catch (err) {
showError(err? err.message : 'Unknown error');
}
}
/**
@ -365,6 +411,7 @@ async function loadWinnerInfo() {
function addChatMessage(messageObject) {
let msgSpan = document.createElement('span');
msgSpan.setAttribute('class', 'chatMessage');
msgSpan.setAttribute('msg-type', messageObject.type);
msgSpan.setAttribute('msg-id', messageObject.id);
if (messageObject.type === "USER") {
msgSpan.innerHTML = `
@ -390,7 +437,7 @@ function addPlayer(player, options) {
playerContainer.setAttribute('b-pid', player.id);
if (options.isAdmin && player.id !== options.admin)
playerContainer.innerHTML = `<button class="kickPlayerButton" onclick="kickPlayer(${player.id})"></button>`;
playerContainer.innerHTML = `<button class="kickPlayerButton" onclick="kickPlayer(${player.id})"></button>`;
playerContainer.innerHTML += `<span class="playernameSpan">${player.username}</span>`;
if (player.id === options.admin)
@ -499,7 +546,7 @@ function checkPlayerRefresh(players) {
if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`))
playerRefresh = true;
if (playerRefresh)
refreshPlayers();
statusWrap(refreshPlayers);
}
/**
@ -512,7 +559,7 @@ function checkMessageRefresh(messages) {
if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`))
messageRefresh = true;
if (messageRefresh)
refreshChat();
statusWrap(refreshChat);
}
/**
@ -613,7 +660,7 @@ async function refreshRound() {
window.addEventListener("unhandledrejection", function (promiseRejectionEvent) {
promiseRejectionEvent.promise.catch(err => console.log(err));
showError('Connection problems... Is the server down?');
showError('Connection problems...');
});
// prevent ctrl + s
@ -622,7 +669,7 @@ window.addEventListener("keydown", async (e) => {
e.preventDefault();
if (document.querySelector('#input-bingo-words')) {
let gridSize = document.querySelector('#input-grid-size').value || 3;
await setLobbySettings(getLobbyWords(), gridSize);
await statusWrap(async () => await setLobbySettings(getLobbyWords(), gridSize));
}
}
}, false);

@ -9,11 +9,12 @@
function postData(url, postBody) {
let request = new XMLHttpRequest();
return new Promise((resolve, reject) => {
let start = new Date().getTime();
request.onload = () => {
resolve({
status: request.status,
data: request.responseText
data: request.responseText,
ping: (new Date().getTime() - start)
});
};
@ -23,7 +24,11 @@ function postData(url, postBody) {
request.open('POST', url, true);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(postBody));
try {
request.send(JSON.stringify(postBody));
} catch (err) {
return err;
}
});
}

@ -45,6 +45,12 @@
color: $primarySurface
border: 1px solid $inactive
.playerWins
margin-left: 1em
.kickPlayerButton
color: $inactive
.chatMessage
display: block
padding: 0.2em
@ -110,14 +116,14 @@
width: 100%
h1
height: 3rem
height: 2rem
margin: 0.5rem
text-align: center
#player-list
padding: 1rem
width: calc(100% - 2rem)
height: calc(100% - 6rem)
height: calc(100% - 5rem)
overflow-y: auto
.kickPlayerButton
@ -128,33 +134,37 @@
font-size: 1em
#container-grid
display: table
height: calc(100% - 2em)
width: calc(100% - 2em)
padding: 1em
overflow: auto
.bingoWordRow
display: table-row
#grid-table
display: table
height: 100%
width: 100%
.bingoWordRow
display: table-row
.bingoWordPanel
@include default-element
display: table-cell
text-align: center
vertical-align: middle
user-select: none
cursor: pointer
.bingoWordPanel
@include default-element
display: table-cell
text-align: center
vertical-align: middle
user-select: none
cursor: pointer
.bingoWordPanel[b-sub='true']
background-color: $success
.bingoWordPanel[b-sub='true']
background-color: $success
.bingoWordPanel:hover
background-color: $primary
.bingoWordPanel:hover
background-color: $primary
.bingoWordPanel[b-sub='true']:hover
background-color: mix($primary, $success)
.bingoWordPanel[b-sub='true']:hover
background-color: mix($primary, $success)
.bingoWordPanel:active
background-color: mix($primary, $secondary)
.bingoWordPanel:active
background-color: mix($primary, $secondary)
#container-bingo-button
height: 100%
@ -164,6 +174,34 @@
height: 100%
width: 100%
#statusbar
display: grid
grid-template: 100% / 1rem calc(80% - 1rem) 20%
background-color: darken($primary, 5%)
margin: 0.5rem 0 0 0
padding: 0.25rem
vertical-align: middle
font-size: 0.8em
text-align: start
#status-indicator
height: 1rem
width: 1rem
border-radius: 100%
@include gridPosition(1, 2, 1,2)
#error-message
@include gridPosition(1, 2, 2, 3)
margin: auto 1em
text-align: start
color: $errorText
#container-info
width: 100%
height: 100%
margin: auto
color: $inactive
/* main containers */
#container-bingo-create
@ -186,13 +224,15 @@
margin: 0 0 0 3em
#lobby-form
@include gridPosition(3, 4, 2, 3)
margin: auto
@include gridPosition(3, 5, 2, 3)
margin: 10% auto
button
width: 100%
#container-bingo-lobby
@include fillWindow
overflow: hidden
display: grid
grid-template: 0 10% 85% 5% / 5% 30% 30% 30% 5%
height: 100%
@ -214,20 +254,29 @@
@include gridPosition(3, 4, 4, 5)
background-color: lighten($primary, 5%)
#statusbar
@include gridPosition(4, 5, 1, 6)
#container-bingo-round
@include fillWindow
overflow: hidden
display: grid
height: 100%
width: 100%
grid-template: 10% 42.5% 42.5% 5% / 25% 75%
grid-template: 10% 30% 55% 5% / 25% 75%
#container-players
@include gridPosition(2, 3, 1, 2)
#container-chat
@include gridPosition(3, 4, 1, 2)
padding: 0 1rem
#container-grid
@include gridPosition(2, 4, 2, 3)
#container-bingo-button
@include gridPosition(1, 2, 1, 2)
#statusbar
@include gridPosition(4, 5, 1, 3)

@ -27,16 +27,17 @@
border-radius: 1em
transition-duration: 1s
.statusIndicator:before
content: ""
.statusIndicator[status='success']
background-color: $success
.statusIndicator[status='success']:before
content: ""
color: $success
.statusIndicator[status='error']
background-color: $error
.statusIndicator[status='error']:before
content: ""
color: $error
.statusIndicator[status='idle']
background-color: $inactive
animation-name: pulse-opacity
animation-duration: 5s
animation-iteration-count: infinite
.statusIndicator[status='pending']
background-color: $pending
@ -44,7 +45,7 @@
animation-duration: 5s
animation-iteration-count: infinite
.idle
.pending
background-color: $pending !important
animation-name: pulse-opacity
animation-duration: 2s

@ -6,6 +6,11 @@
border: 2px solid $primarySurface
transition-duration: 0.2s
@mixin fillWindow
position: absolute
top: 0
left: 0
@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd)
grid-row-start: $rowStart
grid-row-end: $rowEnd

@ -4,7 +4,7 @@
@media (min-device-width: 320px)
html
font-size: 4.5vw
font-size: 4vw
@media (min-device-width: 481px)
html
@ -72,6 +72,9 @@ mark
background-color: $secondary
color: $primarySurface
mark > a
color: white
::-webkit-scrollbar
width: 12px
height: 12px

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

@ -1,5 +1,6 @@
const express = require('express'),
router = express.Router(),
{ GraphQLError } = require('graphql'),
mdEmoji = require('markdown-it-emoji'),
mdMark = require('markdown-it-mark'),
mdSmartarrows = require('markdown-it-smartarrows'),
@ -609,7 +610,16 @@ class GridWrapper {
*/
async toggleField(row, column) {
let result = await bdm.toggleGridFieldSubmitted(this.id, row, column);
return new GridFieldWrapper(result);
let gridField = new GridFieldWrapper(result);
let username = await (await this.player()).username();
let word = await gridField.word.content();
if (gridField.submitted)
await bdm.addInfoMessage(this.lobbyId,
`${username} heared "${word}"`);
else
await bdm.addInfoMessage(this.lobbyId,
`${username} unheared "${word}"`);
return gridField;
}
}
@ -1186,13 +1196,16 @@ function checkBingo(fg) {
*/
async function getPlayerData(lobbyWrapper) {
let playerData = [];
let adminId = (await lobbyWrapper.admin()).id;
for (let player of await lobbyWrapper.players())
playerData.push({
id: player.id,
wins: await player.wins({lobbyId: lobbyWrapper.id}),
username: await player.username()}
);
username: await player.username(),
isAdmin: (player.id === adminId)
});
playerData.sort((a, b) => (a.isAdmin? -1 : (b.wins - a.wins) || a.id));
return playerData;
}
@ -1255,10 +1268,10 @@ router.use(async (req, res, next) => {
router.get('/', async (req, res) => {
let playerId = req.session.bingoPlayerId;
if (!playerId)
req.session.bingoPlayerId = playerId = (await bdm.addPlayer(shuffleArray(playerNames)[0])).id;
// if (!playerId)
// req.session.bingoPlayerId = playerId = (await bdm.addPlayer(shuffleArray(playerNames)[0])).id;
let lobbyWrapper = new LobbyWrapper(req.query.g);
if (req.query.g && await lobbyWrapper.exists()) {
if (playerId && req.query.g && await lobbyWrapper.exists()) {
let lobbyId = req.query.g;
if (!(await lobbyWrapper.roundActive())) {
@ -1338,12 +1351,14 @@ router.graphqlResolver = async (req, res) => {
return new PlayerWrapper(playerId);
},
createLobby: async({gridSize}) => {
if (playerId) {
let result = await bdm.createLobby(playerId, gridSize);
return new LobbyWrapper(result.id);
} else {
res.status(400);
}
if (playerId)
if (gridSize > 0 && gridSize < 10) {
let result = await bdm.createLobby(playerId, gridSize);
return new LobbyWrapper(result.id);
} else {
res.status(413);
}
res.status(400);
},
mutateLobby: async ({id}) => {
let lobbyId = id;
@ -1389,19 +1404,23 @@ router.graphqlResolver = async (req, res) => {
},
setWords: async({words}) => {
let admin = await lobbyWrapper.admin();
if (admin.id === playerId) {
await lobbyWrapper.setWords(words);
return lobbyWrapper;
} else {
res.status(400);
}
if (admin.id === playerId)
if (words.length < 10000) {
await lobbyWrapper.setWords(words);
return lobbyWrapper;
} else {
res.status(413); // request entity too large
}
else
res.status(403); // forbidden
},
sendMessage: async ({message}) => {
if (await lobbyWrapper.hasPlayer(playerId)) {
let result = await bdm.addUserMessage(lobbyId, playerId, message);
return new MessageWrapper(result);
} else {
res.status(400);
res.status(401); // unautorized
}
},
submitBingo: async () => {
@ -1412,9 +1431,10 @@ router.graphqlResolver = async (req, res) => {
if (result)
return currentRound;
else
res.status(400);
res.status(500);
} else {
res.status(400);
return new GraphQLError('Bingo check failed. This is not a bingo!');
}
},
toggleGridField: async ({location}) => {

@ -1,4 +0,0 @@
div(id='container-chat')
//h1(id='chat-header') Chat
div(id='chat-content')
input(id='chat-input' type='text', placeholder='send message', onkeypress='submitOnEnter(event, sendChatMessage)' maxlength="250")

@ -1,4 +1,4 @@
extends bingo-layout
extends includes/bingo-layout
block content
div(id='container-bingo-create')

@ -1,28 +0,0 @@
include bingo-layout
block content
if username === 'anonymous'
div(class='greyover')
div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)')
input(type='text', id='username-input', placeholder=username, maxlength="30")
span Maximum is 30 characters.
button(onclick='submitUsername()') Set Username
div(id='content-container')
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='chat-container')
div(id='chat-content')
input(id='chat-input' type='text', placeholder='chat', onkeypress='submitOnEnter(event, sendChatMessage)' maxlength="250")
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!
div(id='chat-button-container')
button(id='chat-toggle-button', onclick='toggleChatView()') toggle Chat

@ -1,21 +1,22 @@
extends bingo-layout
extends includes/bingo-layout
block content
div(id='container-bingo-lobby')
h1(id='lobby-title') Bingo Lobby
include bingo-players
include includes/bingo-players
div(id='container-lobby-settings')
h1 Words
if isAdmin
span Grid Size:
input(id='input-grid-size' type='number' value=gridSize)
textarea(id='input-bingo-words')= wordString
button(id='button-round-start' onclick='startRound()') Start Round
input(id='input-grid-size' type='number' value=gridSize min='1' max='8')
textarea(id='input-bingo-words' placeholder='max. 10.000 phrases')= wordString
button(id='button-round-start' onclick='statusWrap(startRound)') Start Round
else
div(id='bingo-words')
for word in words
span(class='bingoWord')= word
button(id='button-leave' onclick='leaveLobby()') Leave
include bingo-chat
button(id='button-leave' onclick='statusWrap(leaveLobby)') Leave
include includes/bingo-chat
include includes/bingo-statusbar
script(type='text/javascript') refreshLobby();

@ -1,24 +1,26 @@
include bingo-layout
include includes/bingo-layout
block content
div(id='container-bingo-round')
include bingo-players
include bingo-chat
include includes/bingo-players
include includes/bingo-chat
include includes/bingo-statusbar
if grid.bingo
div(id='container-bingo-button')
button(id='bingo-button' onclick='submitBingo()') Bingo!
button(id='bingo-button' onclick='statusWrap(submitBingo)') Bingo!
else
div(id='container-bingo-button' class='hidden')
button(id='bingo-button' onclick='submitBingo()') Bingo!
button(id='bingo-button' onclick='statusWrap(submitBingo)') Bingo!
div(id='container-grid')
each val in grid.fields
div(class='bingoWordRow')
each field in val
div(
class='bingoWordPanel'
onclick=`submitFieldToggle(this)`
b-row=field.row
b-column=field.column
b-sub=`${field.submitted}`)
span= field.word
div(id='grid-table')
each val in grid.fields
div(class='bingoWordRow')
each field in val
div(
class='bingoWordPanel'
onclick=`statusWrap(() => submitFieldToggle(this))`
b-row=field.row
b-column=field.column
b-sub=`${field.submitted}`)
span= field.word
script(type='text/javascript') refreshRound();

@ -0,0 +1,10 @@
div(id='container-chat')
style(id='js-style')
div(id='chat-content')
input(
id='chat-input'
type='text'
placeholder='send message'
onkeypress='submitOnEnter(event, () => statusWrap(sendChatMessage))'
maxlength="250"
autocomplete='off')

@ -1,6 +1,6 @@
html
head
include ../includes/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')

@ -4,7 +4,11 @@ div(id='container-players')
each player in players
div(class='playerEntryContainer', b-pid=`${player.id}`)
if isAdmin && player.id !== adminId
button(class='kickPlayerButton' onclick=`kickPlayer(${player.id})`) ❌
span(class='playerNameSpan')= player.username
button(class='kickPlayerButton' onclick=`statusWrap(() => kickPlayer(${player.id}))`)
if player.id === adminId
span(class='adminSpan') 👑
span(class='playerNameSpan')= player.username
if player.wins > 1
span(class='playerWins')= `${player.wins}x🌟`
else if player.wins > 0
span(class='playerWins') 🌟

@ -0,0 +1,7 @@
div(id='statusbar')
div(id='status-indicator' class='statusIndicator' status='idle')
span(id='error-message')
span(id='container-info')
| please report bugs
|
a(href='https://github.com/Trivernis/whooshy/issues') here
Loading…
Cancel
Save