diff --git a/CHANGELOG.md b/CHANGELOG.md index ff336d6..365161b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app.js b/app.js index 3f9a49c..771587b 100644 --- a/app.js +++ b/app.js @@ -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'))); diff --git a/misc/usernames.txt b/misc/usernames.txt index 66096b4..a64ade1 100644 --- a/misc/usernames.txt +++ b/misc/usernames.txt @@ -11,3 +11,7 @@ Goblin Slayer useless Aqua theP0wner79 Pr0wn +Cool User 68 +My name Jeff +Bingo Bingo Duolingo +Max Mustermann diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index cc3fff3..e940383 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -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} @@ -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} + * @returns {Promise} */ 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 = ``; + playerContainer.innerHTML = ``; playerContainer.innerHTML += `${player.username}`; 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); diff --git a/public/javascripts/common.js b/public/javascripts/common.js index 91c1c13..f9564b4 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -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; + } }); } diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index a8ba59e..676a751 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -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) diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index c38e044..cd283e0 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -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 diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass index 17fdf5d..e2d7afb 100644 --- a/public/stylesheets/sass/mixins.sass +++ b/public/stylesheets/sass/mixins.sass @@ -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 diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index ec096b1..ffb21f7 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -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 diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass index b88bf64..f35a129 100644 --- a/public/stylesheets/sass/vars.sass +++ b/public/stylesheets/sass/vars.sass @@ -4,5 +4,6 @@ $secondary: teal $borderRadius: 20px $inactive: #aaa $error: #a00 +$errorText: #f44 $success: #0a0 $pending: #aa0 diff --git a/routes/bingo.js b/routes/bingo.js index 1bf6421..7515a14 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -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}) => { diff --git a/views/bingo/bingo-chat.pug b/views/bingo/bingo-chat.pug deleted file mode 100644 index 74027b5..0000000 --- a/views/bingo/bingo-chat.pug +++ /dev/null @@ -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") diff --git a/views/bingo/bingo-create.pug b/views/bingo/bingo-create.pug index c767f52..7013507 100644 --- a/views/bingo/bingo-create.pug +++ b/views/bingo/bingo-create.pug @@ -1,4 +1,4 @@ -extends bingo-layout +extends includes/bingo-layout block content div(id='container-bingo-create') diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug deleted file mode 100644 index 213edc0..0000000 --- a/views/bingo/bingo-game.pug +++ /dev/null @@ -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 diff --git a/views/bingo/bingo-lobby.pug b/views/bingo/bingo-lobby.pug index e46351d..4e559bf 100644 --- a/views/bingo/bingo-lobby.pug +++ b/views/bingo/bingo-lobby.pug @@ -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(); diff --git a/views/bingo/bingo-round.pug b/views/bingo/bingo-round.pug index 45aeb0a..2f42f61 100644 --- a/views/bingo/bingo-round.pug +++ b/views/bingo/bingo-round.pug @@ -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(); diff --git a/views/bingo/includes/bingo-chat.pug b/views/bingo/includes/bingo-chat.pug new file mode 100644 index 0000000..a4eda8a --- /dev/null +++ b/views/bingo/includes/bingo-chat.pug @@ -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') diff --git a/views/bingo/bingo-layout.pug b/views/bingo/includes/bingo-layout.pug similarity index 87% rename from views/bingo/bingo-layout.pug rename to views/bingo/includes/bingo-layout.pug index e0c91ba..d2e2d91 100644 --- a/views/bingo/bingo-layout.pug +++ b/views/bingo/includes/bingo-layout.pug @@ -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') diff --git a/views/bingo/bingo-players.pug b/views/bingo/includes/bingo-players.pug similarity index 55% rename from views/bingo/bingo-players.pug rename to views/bingo/includes/bingo-players.pug index d17f2de..82a5ed8 100644 --- a/views/bingo/bingo-players.pug +++ b/views/bingo/includes/bingo-players.pug @@ -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') 🌟 diff --git a/views/bingo/includes/bingo-statusbar.pug b/views/bingo/includes/bingo-statusbar.pug new file mode 100644 index 0000000..ff83b5e --- /dev/null +++ b/views/bingo/includes/bingo-statusbar.pug @@ -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