From c7507fb4f70f44d76af2be34ad254e0991241fb7 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 9 May 2019 16:20:28 +0200 Subject: [PATCH 01/42] Changes to app - changed js style --- app.js | 16 ++++++++-------- routes/index.js | 6 +++--- routes/users.js | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app.js b/app.js index ab7aed4..2ae8aed 100644 --- a/app.js +++ b/app.js @@ -1,13 +1,13 @@ -var createError = require('http-errors'); -var express = require('express'); -var path = require('path'); -var cookieParser = require('cookie-parser'); -var logger = require('morgan'); +const createError = require('http-errors'), + express = require('express'), + path = require('path'), + cookieParser = require('cookie-parser'), + logger = require('morgan'), -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); + indexRouter = require('./routes/index'), + usersRouter = require('./routes/users'); -var app = express(); +let app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); diff --git a/routes/index.js b/routes/index.js index ecca96a..6aab441 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,9 @@ -var express = require('express'); -var router = express.Router(); +const express = require('express'); +const router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); + res.render('index', { title: 'Trivernis.net' }); }); module.exports = router; diff --git a/routes/users.js b/routes/users.js index 623e430..f15a20d 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,5 +1,5 @@ -var express = require('express'); -var router = express.Router(); +const express = require('express'); +const router = express.Router(); /* GET users listing. */ router.get('/', function(req, res, next) { From 229a6b6ee7a010af661b60dd3587daf90c07ed40 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 9 May 2019 21:48:14 +0200 Subject: [PATCH 02/42] Added riddle - added riddle webpage to download reddit images --- .gitignore | 2 + app.js | 6 +- install.sh | 5 ++ package-lock.json | 28 +++++++++ package.json | 1 + public/javascripts/riddle-web.js | 79 +++++++++++++++++++++++++ routes/riddle.js | 99 ++++++++++++++++++++++++++++++++ views/riddle.pug | 10 ++++ 8 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 install.sh create mode 100644 public/javascripts/riddle-web.js create mode 100644 routes/riddle.js create mode 100644 views/riddle.pug diff --git a/.gitignore b/.gitignore index 91bc7f4..eda6428 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ package-lock bin .idea node_modules +scripts/* +tmp diff --git a/app.js b/app.js index 2ae8aed..6b35600 100644 --- a/app.js +++ b/app.js @@ -4,8 +4,9 @@ const createError = require('http-errors'), cookieParser = require('cookie-parser'), logger = require('morgan'), - indexRouter = require('./routes/index'), - usersRouter = require('./routes/users'); + indexRouter = require('./routes/index'), + usersRouter = require('./routes/users'), + riddleRouter = require('./routes/riddle'); let app = express(); @@ -21,6 +22,7 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); +app.use(/\/riddle(\/.*)?/, riddleRouter); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..3bb2bb6 --- /dev/null +++ b/install.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +npm i +git clone https://github.com/trivernis/reddit-riddle ./scripts/reddit-riddle +pip3 install -r ./scripts/reddit-riddle/requirements.txt +mkdir tmp diff --git a/package-lock.json b/package-lock.json index 63645dc..0cbd297 100644 --- a/package-lock.json +++ b/package-lock.json @@ -334,11 +334,26 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, "graceful-readlink": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", @@ -420,6 +435,14 @@ "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", @@ -819,6 +842,11 @@ "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", "optional": true }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 5ac7627..8a65a6a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", + "fs-extra": "^7.0.1", "http-errors": "~1.6.3", "morgan": "~1.9.1", "pug": "2.0.0-beta11" diff --git a/public/javascripts/riddle-web.js b/public/javascripts/riddle-web.js new file mode 100644 index 0000000..29e1219 --- /dev/null +++ b/public/javascripts/riddle-web.js @@ -0,0 +1,79 @@ +function postLocData(postBody) { + let request = new XMLHttpRequest(); + return new Promise((res, rej) => { + + request.onload = () => { + res({ + status: request.status, + data: request.responseText + }); + }; + + request.onerror = () => { + rej(request.error); + }; + + request.open('POST', '#', true); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + request.send(JSON.stringify(postBody)); + }); +} + +async function startSubredditDownload(subredditName) { + let data = await postLocData({ + subreddit: subredditName + }); + return JSON.parse(data.data); +} + +async function getDownloadStatus(downloadId) { + let data = await postLocData({ + id: downloadId + }); + return JSON.parse(data.data); +} + +async function refreshDownloadInfo(downloadId) { + let response = await getDownloadStatus(downloadId); + + let dlDiv = document.querySelector(`.download-container[dl-id='${downloadId}']`); + dlDiv.querySelector('.downloadStatus').innerText = response.status; + let subredditName = dlDiv.getAttribute('subreddit-name'); + + if (response.status === 'pending') { + setTimeout(() => refreshDownloadInfo(downloadId), 1000) + } else { + let dlLink = document.createElement('a'); + dlLink.setAttribute('href', response.file); + dlLink.setAttribute('filename', `${subredditName}`); + for (let cNode of dlDiv.childNodes) + dlLink.appendChild(cNode); + dlDiv.appendChild(dlLink); + setTimeout(() => { + dlDiv.remove(); + }, 30000); + } +} + +async function submitDownload() { + let subredditName = document.querySelector('#subreddit-input').value; + let response = await startSubredditDownload(subredditName); + + let dlDiv = document.createElement('div'); + dlDiv.setAttribute('class', 'download-container'); + dlDiv.setAttribute('dl-id', response.id); + dlDiv.setAttribute('subreddit-name', subredditName); + document.querySelector('#download-list').prepend(dlDiv); + + let subnameSpan = document.createElement('span'); + subnameSpan.innerText = subredditName; + subnameSpan.setAttribute('class', 'subredditName'); + dlDiv.appendChild(subnameSpan); + + let dlStatusSpan = document.createElement('span'); + dlStatusSpan.innerText = response.status; + dlStatusSpan.setAttribute('class', 'downloadStatus'); + dlDiv.appendChild(dlStatusSpan); + + await refreshDownloadInfo(response.id); +} diff --git a/routes/riddle.js b/routes/riddle.js new file mode 100644 index 0000000..3ce0092 --- /dev/null +++ b/routes/riddle.js @@ -0,0 +1,99 @@ +const express = require('express'), + router = express.Router(), + cproc = require('child_process'), + fsx = require('fs-extra'); + +const rWordOnly = /^\w+$/; + +let downloads = {}; + +class RedditDownload { + constructor(file) { + this.file = file; + this.status = 'pending'; + this.progress = 'N/A'; + this.process = null; + } +} + +/** + * Generates an id for a subreddit download. + * @param subreddit + * @returns {string} + */ +function generateDownloadId(subreddit) { + return Date.now().toString(16); +} + +/** + * Starts the subreddit download by executing the riddle python file. + * @param subreddit {String} + * @returns {string} + */ +function startDownload(subreddit) { + if (rWordOnly.test(subreddit)) { + let downloadId = generateDownloadId(subreddit); + let dlFilePath = `./public/static/${downloadId}.zip`; + let dlWebPath = `/static/${downloadId}.zip`; + let dl = new RedditDownload(dlWebPath); + + dl.process = cproc.exec(`python -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`, + {cwd: './scripts/reddit-riddle', env: {PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: true}}, + (err, stdout) => { + if (err) { + console.error(err); + } else { + console.log(`riddle.py: ${stdout}`); + } + }); + + dl.process.on('exit', (code) => { + if (code === 0) + dl.status = 'finished'; + else + dl.status = 'failed'; + setTimeout(async () => { + await fsx.remove(dlFilePath); + delete downloads[downloadId]; + }, 300000); // delete the file after 5 minutes + }); + + dl.process.on('message', (msg) => { + console.log(msg) + }); + + downloads[downloadId] = dl; + + return downloadId; + } +} + +router.use('/files', express.static('./tmp')); + +router.get('/', (req, res, next) => { + res.render('riddle'); +}); + +router.post('/', (req, res) => { + if (req.body.subreddit) { + let id = startDownload(req.body.subreddit); + let download = downloads[id]; + + res.send({id: id, status: download.status, file: download.file}); + } else if (req.body.id) { + let id = req.body.id; + let download = downloads[id]; + + if (download) { + res.send({ + id: id, + status: download.status, + file: download.file + }); + } else { + res.send({error: 'Unknown download ID', id: id}); + } + } +}); + +module.exports = router; diff --git a/views/riddle.pug b/views/riddle.pug new file mode 100644 index 0000000..f710a6f --- /dev/null +++ b/views/riddle.pug @@ -0,0 +1,10 @@ +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + script(type='text/javascript', src='/javascripts/riddle-web.js') + body + h1 Riddle Reddit downloader + input(type='text' placeholder='subreddit' id='subreddit-input') + button(id='submit-download' onclick='submitDownload()') Download + div(id='download-list') From 5cde198890500979b2564212fda72fb89fa674df Mon Sep 17 00:00:00 2001 From: Julius Date: Fri, 10 May 2019 16:58:30 +0200 Subject: [PATCH 03/42] Added bingo - added bingo front and backend script - added sessions - added configuration files (default-config.yaml is needed, config.yaml for custom configuration) --- .gitignore | 1 + app.js | 28 ++- default-config.yaml | 5 + package.json | 5 +- public/javascripts/bingo-web.js | 88 ++++++++ public/javascripts/common.js | 45 ++++ public/javascripts/riddle-web.js | 35 +-- public/stylesheets/sass/bingo/style.sass | 62 ++++++ public/stylesheets/sass/classes.sass | 13 ++ public/stylesheets/sass/mixins.sass | 7 + public/stylesheets/sass/riddle/style.sass | 22 ++ public/stylesheets/sass/style.sass | 28 +++ public/stylesheets/sass/vars.sass | 4 + routes/bingo.js | 259 ++++++++++++++++++++++ routes/riddle.js | 2 +- views/bingo/bingo-game.pug | 13 ++ views/bingo/bingo-layout.pug | 8 + views/bingo/bingo-submit.pug | 9 + views/includes/head.pug | 2 + views/riddle.pug | 3 +- 20 files changed, 606 insertions(+), 33 deletions(-) create mode 100644 default-config.yaml create mode 100644 public/javascripts/bingo-web.js create mode 100644 public/javascripts/common.js create mode 100644 public/stylesheets/sass/bingo/style.sass create mode 100644 public/stylesheets/sass/classes.sass create mode 100644 public/stylesheets/sass/mixins.sass create mode 100644 public/stylesheets/sass/riddle/style.sass create mode 100644 public/stylesheets/sass/style.sass create mode 100644 public/stylesheets/sass/vars.sass create mode 100644 routes/bingo.js create mode 100644 views/bingo/bingo-game.pug create mode 100644 views/bingo/bingo-layout.pug create mode 100644 views/bingo/bingo-submit.pug create mode 100644 views/includes/head.pug diff --git a/.gitignore b/.gitignore index eda6428..761a310 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ bin node_modules scripts/* tmp +config.yaml \ No newline at end of file diff --git a/app.js b/app.js index 6b35600..f08a7fc 100644 --- a/app.js +++ b/app.js @@ -3,26 +3,50 @@ const createError = require('http-errors'), path = require('path'), cookieParser = require('cookie-parser'), logger = require('morgan'), + compileSass = require('express-compile-sass'), + session = require('express-session'), + fsx = require('fs-extra'), + yaml = require('js-yaml'), indexRouter = require('./routes/index'), usersRouter = require('./routes/users'), - riddleRouter = require('./routes/riddle'); + riddleRouter = require('./routes/riddle'), + bingoRouter = require('./routes/bingo'); + +let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); + +if (fsx.existsSync('config.yaml')) + Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); let app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); +app.set('trust proxy', 1); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); +app.use(session({ + secret: settings.sessions.secret, + resave: false, + saveUninitialized: true, + cookie: { maxAge: settings.sessions.maxAge } +})); +app.use('/sass', compileSass({ + root: './public/stylesheets/sass', + sourceMap: true, + watchFiles: true, + logToConsole: true +})); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); app.use(/\/riddle(\/.*)?/, riddleRouter); +app.use(/\/bingo?.*/, bingoRouter); // catch 404 and forward to error handler app.use(function(req, res, next) { @@ -40,4 +64,4 @@ app.use(function(err, req, res, next) { res.render('error'); }); -module.exports = app; +app.listen(settings.port); diff --git a/default-config.yaml b/default-config.yaml new file mode 100644 index 0000000..9edb835 --- /dev/null +++ b/default-config.yaml @@ -0,0 +1,5 @@ +sessions: + secret: averysecuresessionsecret + maxAge: 1000000 + +port: 3000 \ No newline at end of file diff --git a/package.json b/package.json index 8a65a6a..9fb1f5f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "fs-extra": "^7.0.1", "http-errors": "~1.6.3", "morgan": "~1.9.1", - "pug": "2.0.0-beta11" + "pug": "2.0.0-beta11", + "express-compile-sass": "latest", + "express-session": "latest", + "js-yaml": "latest" } } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js new file mode 100644 index 0000000..4acfa78 --- /dev/null +++ b/public/javascripts/bingo-web.js @@ -0,0 +1,88 @@ +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); +} + +async function submitUsername() { + let username = document.querySelector('#username-input').value; + let response = await postLocData({ + username: username + }); + + console.log(response); +} + +async function submitWord(word) { + let response = await postLocData({ + bingoWord: word + }); + console.log(response); + + let data = JSON.parse(response.data); + for (let row of data.fieldGrid) { + for (let field of row) { + document.querySelector(`.bingo-word-panel[b-word="${field.word}"]`) + .setAttribute('b-sub', field.submitted); + } + } + if (data.bingo) { + document.querySelector('#bingo-button').setAttribute('class', ''); + } +} + +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) + } + console.log(response); +} + +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) + } + console.log(response); +} + +function displayWinner(name) { + let winnerDiv = document.createElement('div'); + let greyoverDiv = document.createElement('div'); + let winnerSpan = document.createElement('span'); + winnerDiv.setAttribute('class', 'popup'); + greyoverDiv.setAttribute('class', 'greyover'); + winnerSpan.innerText = `${name} has won!`; + winnerDiv.appendChild(winnerSpan); + document.body.append(greyoverDiv); + document.body.appendChild(winnerDiv); +} + +window.onload = () => { + if (window && !document.querySelector('#bingoform')) { + refrInterval = setInterval(refresh, 1000); + } + let gridSizeElem = document.querySelector('#bingo-grid-size'); + document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; + gridSizeElem.oninput = () => { + document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; + }; +}; \ No newline at end of file diff --git a/public/javascripts/common.js b/public/javascripts/common.js new file mode 100644 index 0000000..0883713 --- /dev/null +++ b/public/javascripts/common.js @@ -0,0 +1,45 @@ +function postLocData(postBody) { + let request = new XMLHttpRequest(); + return new Promise((res, rej) => { + + request.onload = () => { + res({ + status: request.status, + data: request.responseText + }); + }; + + request.onerror = () => { + rej(request.error); + }; + + request.open('POST', '#', true); + request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + request.send(JSON.stringify(postBody)); + }); +} + +function insertParam(key, value) { + key = encodeURI(key); + value = encodeURI(value); + + let kvp = document.location.search.substr(1).split('&'); + + let i = kvp.length; + let x; + while (i--) { + x = kvp[i].split('='); + + if (x[0] === key) { + x[1] = value; + kvp[i] = x.join('='); + break; + } + } + + if (i < 0) { + kvp[kvp.length] = [key, value].join('='); + } + + document.location.search = kvp.join('&'); +} \ No newline at end of file diff --git a/public/javascripts/riddle-web.js b/public/javascripts/riddle-web.js index 29e1219..3329b11 100644 --- a/public/javascripts/riddle-web.js +++ b/public/javascripts/riddle-web.js @@ -1,24 +1,3 @@ -function postLocData(postBody) { - let request = new XMLHttpRequest(); - return new Promise((res, rej) => { - - request.onload = () => { - res({ - status: request.status, - data: request.responseText - }); - }; - - request.onerror = () => { - rej(request.error); - }; - - request.open('POST', '#', true); - request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - request.send(JSON.stringify(postBody)); - }); -} - async function startSubredditDownload(subredditName) { let data = await postLocData({ subreddit: subredditName @@ -45,13 +24,13 @@ async function refreshDownloadInfo(downloadId) { } else { let dlLink = document.createElement('a'); dlLink.setAttribute('href', response.file); - dlLink.setAttribute('filename', `${subredditName}`); - for (let cNode of dlDiv.childNodes) - dlLink.appendChild(cNode); + dlLink.setAttribute('download', `${subredditName}`); + dlLink.innerHTML = dlDiv.innerHTML; + dlDiv.innerHTML = ''; dlDiv.appendChild(dlLink); setTimeout(() => { dlDiv.remove(); - }, 30000); + }, 300000); } } @@ -66,13 +45,13 @@ async function submitDownload() { document.querySelector('#download-list').prepend(dlDiv); let subnameSpan = document.createElement('span'); - subnameSpan.innerText = subredditName; - subnameSpan.setAttribute('class', 'subredditName'); + subnameSpan.innerText = 'r/'+subredditName; + subnameSpan.setAttribute('class', 'subredditName tableRow'); dlDiv.appendChild(subnameSpan); let dlStatusSpan = document.createElement('span'); dlStatusSpan.innerText = response.status; - dlStatusSpan.setAttribute('class', 'downloadStatus'); + dlStatusSpan.setAttribute('class', 'downloadStatus tableRow'); dlDiv.appendChild(dlStatusSpan); await refreshDownloadInfo(response.id); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass new file mode 100644 index 0000000..1761747 --- /dev/null +++ b/public/stylesheets/sass/bingo/style.sass @@ -0,0 +1,62 @@ +@import ../mixins +@import ../vars + +button + margin: 1rem + +textarea + @include default-element + display: block + margin: 1rem + border-radius: 0 + height: 50% + width: 50% + font-size: 15pt + +.number-input + width: 4rem + margin: 1rem + +#words-container + display: table + + .bingo-word-row + display: table-row + + .bingo-word-panel + @include default-element + display: table-cell + padding: 3rem + transition-duration: 0.3s + max-width: 15rem + + .bingo-word-panel:hover + background-color: darken($primary, 2%) + cursor: pointer + + .bingo-word-panel:active + background-color: $primary + + .bingo-word-panel[b-sub="true"] + background-color: forestgreen + +.popup + @include default-element + height: 5% + width: 40% + top: 47.5% + left: 30% + text-align: center + transition-duration: 1s + span + margin: 2% + display: block + +.greyover + width: 100% + height: 100% + position: fixed + z-index: 99 + top: 0 + left: 0 + background-color: transparentize($primary, 0.5) \ No newline at end of file diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass new file mode 100644 index 0000000..0a6474d --- /dev/null +++ b/public/stylesheets/sass/classes.sass @@ -0,0 +1,13 @@ +.tableRow + display: table-row + +.hidden + display: None + +.popup + height: 60% + width: 40% + z-index: 1000 + position: fixed + top: 20% + left: 30% \ No newline at end of file diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass new file mode 100644 index 0000000..82a0018 --- /dev/null +++ b/public/stylesheets/sass/mixins.sass @@ -0,0 +1,7 @@ +@import vars + +@mixin default-element + background: lighten($primary, 10%) + color: $primarySurface + border: 2px solid $primarySurface + border-radius: $borderRadius \ No newline at end of file diff --git a/public/stylesheets/sass/riddle/style.sass b/public/stylesheets/sass/riddle/style.sass new file mode 100644 index 0000000..159be34 --- /dev/null +++ b/public/stylesheets/sass/riddle/style.sass @@ -0,0 +1,22 @@ +@import ../mixins +@import ../vars + +#download-list + margin: 1rem 0 + +.download-container + @include default-element + display: inline-block + margin: 1rem + padding: 1rem + .subredditName + font-weight: bold + a + text-decoration: none + color: $primarySurface + +#submit-download + margin: 0 1rem + +#subreddit-input + margin: 0 1rem \ No newline at end of file diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass new file mode 100644 index 0000000..653cfd1 --- /dev/null +++ b/public/stylesheets/sass/style.sass @@ -0,0 +1,28 @@ +@import vars +@import classes +@import mixins + +body + background-color: $primary + color: $primarySurface + font-size: 18pt + font-family: Arial, sans-serif + +button + @include default-element + font-size: 20pt + padding: 10px + transition-duration: 0.2s + +button:hover + background-color: darken($primary, 2%) + cursor: pointer + +button:active + background-color: lighten($primary, 15%) + +input + @include default-element + font-size: 20pt + background-color: lighten($primary, 10%) + padding: 9px \ No newline at end of file diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass new file mode 100644 index 0000000..9f8b9b2 --- /dev/null +++ b/public/stylesheets/sass/vars.sass @@ -0,0 +1,4 @@ +$primary: #223 +$primarySurface: white + +$borderRadius: 20px \ No newline at end of file diff --git a/routes/bingo.js b/routes/bingo.js new file mode 100644 index 0000000..684fb17 --- /dev/null +++ b/routes/bingo.js @@ -0,0 +1,259 @@ +const express = require('express'), + router = express.Router(), + cproc = require('child_process'), + fsx = require('fs-extra'); + +const rWordOnly = /^\w+$/; + +let bingoSessions = {}; + +class BingoSession { + /** + * constructor + * @param words List + * @param [size] Number + */ + constructor(words, size = 3) { + this.id = generateBingoId(); + this.words = words; + this.gridSize = size; + this.users = {}; + this.bingos = []; // array with the users that already had bingo + this.finished = false; + } + + /** + * Adds a user to the session + * @param user + */ + addUser(user) { + let id = user.id; + this.users[id] = user; + } +} + +class BingoUser { + constructor() { + this.id = generateBingoId(); + this.game = null; + this.username = 'anonymous'; + this.grids = {}; + this.submittedWords = {}; + } +} + +class BingoWordField { + constructor(word) { + this.word = word; + this.submitted = false; + } +} + +class BingoGrid { + constructor(wordGrid) { + this.wordGrid = wordGrid; + this.fieldGrid = wordGrid.map(x => x.map(y => new BingoWordField(y))); + return this; + } +} + +/** + * Shuffles the elements in an array + * @param array {Array<*>} + * @returns {Array<*>} + */ +function shuffleArray(array) { + let counter = array.length; + while (counter > 0) { + let index = Math.floor(Math.random() * counter); + counter--; + let temp = array[counter]; + array[counter] = array[index]; + array[index] = temp; + } + return array; +} + +/** + * Generates an id for a subreddit download. + * @returns {string} + */ +function generateBingoId() { + return Date.now().toString(16); +} + +/** + * Generates a word grid with random word placements in the given dimensions + * @param dimensions {Array} - the dimensions of the grid + * @param words {Array} - the words included in the grid + * @returns {BingoGrid} + */ +function generateWordGrid(dimensions, words) { + let shuffledWords = shuffleArray(words); + let grid = []; + for (let x = 0; x < dimensions[1]; x++) { + grid[x] = []; + for (let y = 0; y < dimensions[0]; y++) { + grid[x][y] = shuffledWords[(x * dimensions[0]) + y]; + } + } + return (new BingoGrid(grid)); +} + +/** + * Sets the submitted parameter of the words in the bingo grid that match to true. + * @param word {String} + * @param bingoGrid {BingoGrid} + * @returns {boolean} + */ +function submitWord(word, bingoGrid) { + let results = bingoGrid.fieldGrid.find(x => x.find(y => (y.word === word))).find(x => x.word === word); + + if (results) { + (results instanceof Array)? results.forEach(x => {x.submitted = true}): results.submitted = true; + checkBingo(bingoGrid); + return true; + } + return false; +} + +/** + * Checks if a bingo exists in the bingo grid. + * @param bingoGrid {BingoGrid} + * @returns {boolean} + */ +function checkBingo(bingoGrid) { + let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted)); + + let diagonalBingo = true; + // diagonal check + for (let i = 0; i < fg.length; i++) + diagonalBingo = fg[i][i] && diagonalBingo; + if (diagonalBingo) { + bingoGrid.bingo = true; + return true; + } + diagonalBingo = true; + for (let i = 0; i < fg.length; i++) + diagonalBingo = fg[i][fg.length - i - 1] && diagonalBingo; + if (diagonalBingo) { + bingoGrid.bingo = true; + return true; + } + let bingoCheck = true; + // horizontal check + for (let row of fg) { + bingoCheck = true; + for (let field of row) + bingoCheck = field && bingoCheck; + if (bingoCheck) + break; + } + if (bingoCheck) { + bingoGrid.bingo = true; + return true; + } + bingoCheck = true; + // vertical check + for (let i = 0; i < fg.length; i++) { + bingoCheck = true; + for (let j = 0; j < fg.length; j++) + bingoCheck = fg[j][i] && bingoCheck; + if (bingoCheck) + break; + } + if (bingoCheck) { + bingoGrid.bingo = true; + return true; + } + return false; +} + +// -- Router stuff + +router.use((req, res, next) => { + if (!req.session.bingoUser) { + req.session.bingoUser = new BingoUser(); + } + next(); +}); + +router.get('/', (req, res) => { + let bingoUser = req.session.bingoUser; + if (req.query.game) { + let gameId = req.query.game; + + if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { + bingoUser.game = gameId; + let bingoSession = bingoSessions[gameId]; + bingoSession.addUser(bingoUser); + + if (!bingoUser.grids[gameId]) { + bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words); + } + res.render('bingo/bingo-game', {grid: bingoUser.grids[gameId].wordGrid, username: bingoUser.username}); + } else { + res.render('bingo/bingo-submit'); + } + } else { + res.render('bingo/bingo-submit'); + } +}); + +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) { + if (!bingoUser.submittedWords[gameId]) + bingoUser.submittedWords[gameId] = []; + bingoUser.submittedWords[gameId].push(data.bingoWord); + console.log(typeof bingoUser.grids[gameId]); + if (bingoUser.grids[gameId]) + submitWord(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[game.id]; + }, 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' + }) + } +}); + +module.exports = router; diff --git a/routes/riddle.js b/routes/riddle.js index 3ce0092..6d23413 100644 --- a/routes/riddle.js +++ b/routes/riddle.js @@ -37,7 +37,7 @@ function startDownload(subreddit) { let dlWebPath = `/static/${downloadId}.zip`; let dl = new RedditDownload(dlWebPath); - dl.process = cproc.exec(`python -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`, + dl.process = cproc.exec(`python3 -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`, {cwd: './scripts/reddit-riddle', env: {PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: true}}, (err, stdout) => { if (err) { diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug new file mode 100644 index 0000000..d0e2f31 --- /dev/null +++ b/views/bingo/bingo-game.pug @@ -0,0 +1,13 @@ +include bingo-layout + +block content + div(id='username-form') + input(type='text', id='username-input', placeholder='username', value=username) + button(onclick='submitUsername()') Set Username + button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! + div(id='words-container') + each val in grid + div(class='bingo-word-row') + each word in val + div(class='bingo-word-panel', onclick=`submitWord('${word}')`, b-word=word, b-sub='false') + span= word \ No newline at end of file diff --git a/views/bingo/bingo-layout.pug b/views/bingo/bingo-layout.pug new file mode 100644 index 0000000..7f6f4a4 --- /dev/null +++ b/views/bingo/bingo-layout.pug @@ -0,0 +1,8 @@ +html + head + include ../includes/head + script(type='text/javascript', src='/javascripts/bingo-web.js') + link(rel='stylesheet', href='/sass/bingo/style.sass') + + body + block content \ No newline at end of file diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug new file mode 100644 index 0000000..577cc1e --- /dev/null +++ b/views/bingo/bingo-submit.pug @@ -0,0 +1,9 @@ +extends bingo-layout + +block content + div(id='bingoform') + input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8) + span x + span(id='bingo-grid-y', class='number-input') 3 + button(onclick='submitBingoWords()') Submit + textarea(id='bingo-textarea', placeholder='Bingo Words') \ No newline at end of file diff --git a/views/includes/head.pug b/views/includes/head.pug new file mode 100644 index 0000000..2c11aef --- /dev/null +++ b/views/includes/head.pug @@ -0,0 +1,2 @@ +link(rel='stylesheet', href='/sass/style.sass') +script(type='text/javascript', src='/javascripts/common.js') \ No newline at end of file diff --git a/views/riddle.pug b/views/riddle.pug index f710a6f..d17ed94 100644 --- a/views/riddle.pug +++ b/views/riddle.pug @@ -1,8 +1,9 @@ html head title= title - link(rel='stylesheet', href='/stylesheets/style.css') + include includes/head.pug script(type='text/javascript', src='/javascripts/riddle-web.js') + link(rel='stylesheet', href='/sass/riddle/style.sass') body h1 Riddle Reddit downloader input(type='text' placeholder='subreddit' id='subreddit-input') From 1e96cbe68efb28222c6a7fea31d8813056b9442c Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 00:03:28 +0200 Subject: [PATCH 04/42] Back- and frontend improvements - added graphql backend - added mobile support for frontend - improved username form - improved access to html-elements - added toggle to bingo fields --- .gitignore | 3 +- app.js | 27 +- bin/www | 90 + graphql/bingo.graphql | 82 + graphql/schema.graphql | 11 + package-lock.json | 3008 +++++++++++++++++++++- package.json | 12 +- public/javascripts/bingo-web.js | 16 +- public/stylesheets/sass/bingo/style.sass | 74 +- public/stylesheets/sass/mixins.sass | 3 +- public/stylesheets/sass/style.sass | 33 +- routes/bingo.js | 150 +- views/bingo/bingo-game.pug | 16 +- views/bingo/bingo-layout.pug | 3 +- views/bingo/bingo-submit.pug | 13 +- 15 files changed, 3448 insertions(+), 93 deletions(-) create mode 100644 bin/www create mode 100644 graphql/bingo.graphql create mode 100644 graphql/schema.graphql diff --git a/.gitignore b/.gitignore index 761a310..adda588 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ package-lock -bin .idea node_modules scripts/* tmp -config.yaml \ No newline at end of file +config.yaml diff --git a/app.js b/app.js index f08a7fc..6fa7cdd 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,9 @@ const createError = require('http-errors'), session = require('express-session'), fsx = require('fs-extra'), yaml = require('js-yaml'), + graphqlHTTP = require('express-graphql'), + { buildSchema } = require('graphql'), + { importSchema } = require('graphql-import'), indexRouter = require('./routes/index'), usersRouter = require('./routes/users'), @@ -18,6 +21,12 @@ 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) => { + return { + time: Date.now(), + bingo: bingoRouter.graphqlResolver(request) + } +}; let app = express(); // view engine setup @@ -33,7 +42,9 @@ app.use(session({ secret: settings.sessions.secret, resave: false, saveUninitialized: true, - cookie: { maxAge: settings.sessions.maxAge } + cookie: { + expires: 10000000 + } })); app.use('/sass', compileSass({ root: './public/stylesheets/sass', @@ -46,7 +57,15 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); app.use(/\/riddle(\/.*)?/, riddleRouter); -app.use(/\/bingo?.*/, bingoRouter); +app.use('/bingo', bingoRouter); +app.use('/graphql', graphqlHTTP(request => { + return { + schema: buildSchema(importSchema('./graphql/schema.graphql')), + rootValue: graphqlResolver(request), + context: {session: request.session}, + graphiql: true + }; +})); // catch 404 and forward to error handler app.use(function(req, res, next) { @@ -64,4 +83,6 @@ app.use(function(err, req, res, next) { res.render('error'); }); -app.listen(settings.port); +module.exports = app; + +//app.listen(settings.port); diff --git a/bin/www b/bin/www new file mode 100644 index 0000000..8c61406 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('whooshy:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql new file mode 100644 index 0000000..7270897 --- /dev/null +++ b/graphql/bingo.graphql @@ -0,0 +1,82 @@ +type BingoMutation { + + # creates a game of bingo and returns the game id + createGame(words: [String!]!, size: Int = 3): BingoGame + + # submit a bingo to the active game session + submitBingo: Boolean + + # toggle a word (heared or not) on the sessions grid + toggleWord(word: String, base64Word: String): BingoGrid + + # set the username of the current session + setUsername(username: String!): BingoUser +} + +type BingoQuery { + + # Returns the currently active bingo game + gameInfo(id: ID): BingoGame + + # If there is a bingo in the fields. + checkBingo: Boolean + + # Returns the grid of the active bingo game + activeGrid: BingoGrid +} + +type BingoGame { + + # the id of the bingo game + id: ID! + + # the words used in the bingo game + words: [String]! + + # the size of the square-grid + gridSize: Int + + # an array of players active in the bingo game + players(id: ID): [BingoUser] + + # the player-ids that scored a bingo + bingos: [String]! + + # if the game has already finished + finished: Boolean +} + +type BingoUser { + + # the id of the bingo user + id: ID! + + # the id of the currently active bingo game + game: ID + + # the name of the user + username: String +} + +type BingoGrid { + + # the grid represented as string matrix + wordGrid: [[String]]! + + # the grid represented as bingo field matrix + fieldGrid: [[BingoField]]! + + # if there is a bingo + bingo: Boolean +} + +type BingoField { + + # the word contained in the bingo field + word: String + + # if the word was already heared + submitted: Boolean! + + base64Word: String +} diff --git a/graphql/schema.graphql b/graphql/schema.graphql new file mode 100644 index 0000000..a5ac58a --- /dev/null +++ b/graphql/schema.graphql @@ -0,0 +1,11 @@ +# import BingoMutation from 'bingo.graphql' +# import BingoQuery from 'bingo.graphql' + +type Query { + time: String + bingo: BingoQuery +} + +type Mutation { + bingo: BingoMutation +} diff --git a/package-lock.json b/package-lock.json index 0cbd297..389a636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,11 @@ "@types/babel-types": "*" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -46,6 +51,17 @@ } } }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -61,16 +77,143 @@ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -96,6 +239,66 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -104,6 +307,27 @@ "safe-buffer": "5.1.2" } }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "~2.0.0" + } + }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -121,16 +345,89 @@ "type-is": "~1.6.16" } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + } + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -140,6 +437,16 @@ "lazy-cache": "^1.0.3" } }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -148,6 +455,46 @@ "is-regex": "^1.0.3" } }, + "chokidar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", + "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "clean-css": { "version": "3.4.28", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", @@ -167,6 +514,41 @@ "wordwrap": "0.0.2" } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", @@ -175,6 +557,21 @@ "graceful-readlink": ">= 1.0.0" } }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "constantinople": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", @@ -215,11 +612,51 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, "core-js": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "csserror": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/csserror/-/csserror-2.0.2.tgz", + "integrity": "sha1-c9SDIAlNYQQLF9Ya2AhjH3uvwXQ=" + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -233,6 +670,63 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -248,6 +742,15 @@ "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -258,11 +761,29 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -273,6 +794,38 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "express": { "version": "4.16.4", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", @@ -310,6 +863,198 @@ "vary": "~1.1.2" } }, + "express-compile-sass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-compile-sass/-/express-compile-sass-4.0.0.tgz", + "integrity": "sha512-gcFZ3LCGCb553C4wh253hUkQS6C+bMgmLRoMyY17vcBVuATwCyocmOO/pS3Fmlarn1E9ssHJvkvcAOSW4M/jXA==", + "requires": { + "chalk": "^2.3.1", + "chokidar": "^2.0.1", + "csserror": "^2.0.2", + "inline-source-map-comment": "^1.0.5" + } + }, + "express-graphql": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/express-graphql/-/express-graphql-0.8.0.tgz", + "integrity": "sha512-yjFFLTw9J/7QbLBs/SRX1IykEnJHIdaAeDTiw1yC+zPHoQSCijKdPZYhe+B0SC0sFJydAXW6RSqqnERGgKPH7g==", + "requires": { + "accepts": "^1.3.5", + "content-type": "^1.0.4", + "http-errors": "^1.7.2", + "raw-body": "^2.3.3" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } + } + }, + "express-session": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.1.tgz", + "integrity": "sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.2", + "safe-buffer": "5.1.2", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "finalhandler": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", @@ -324,11 +1069,48 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -344,41 +1126,700 @@ "universalify": "^0.1.0" } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "function-bind": "^1.1.1" + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "globule": "^1.0.0" } }, - "iconv-lite": { + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "graphql": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.3.0.tgz", + "integrity": "sha512-MdfI4v7kSNC3NhB7cF8KNijDsifuWO2XOtzpyququqaclO8wVuChYv+KogexDwgP5sp7nFI9Z6N4QHgoLkfjrg==", + "requires": { + "iterall": "^1.2.2" + } + }, + "graphql-import": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", + "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", + "requires": { + "lodash": "^4.17.4", + "resolve-from": "^4.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", @@ -386,21 +1827,130 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "inline-source-map-comment": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz", + "integrity": "sha1-UKikTCp5DfrEQbXJTszVRiY1+vY=", + "requires": { + "chalk": "^1.0.0", + "get-stdin": "^4.0.1", + "minimist": "^1.1.1", + "sum-up": "^1.0.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, "is-expression": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", @@ -417,6 +1967,56 @@ } } }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", @@ -430,11 +2030,85 @@ "has": "^1.0.1" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "iterall": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", + "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==" + }, "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -443,6 +2117,17 @@ "graceful-fs": "^4.1.6" } }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "jstransformer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", @@ -465,6 +2150,26 @@ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", @@ -475,11 +2180,64 @@ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -490,6 +2248,33 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", @@ -508,6 +2293,53 @@ "mime-db": "1.40.0" } }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, "morgan": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", @@ -525,16 +2357,201 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + } + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -548,20 +2565,129 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "promise": { "version": "7.3.1", @@ -580,6 +2706,16 @@ "ipaddr.js": "1.9.0" } }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, "pug": { "version": "2.0.0-beta11", "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.0-beta11.tgz", @@ -694,11 +2830,21 @@ "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz", "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -715,16 +2861,132 @@ "unpipe": "1.0.0" } }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, "resolve": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", @@ -733,6 +2995,21 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -741,16 +3018,94 @@ "align-text": "^0.1.1" } }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + } + } + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, "send": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", @@ -782,11 +3137,139 @@ "send": "0.16.2" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + } + }, "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", @@ -795,21 +3278,289 @@ "amdefine": ">=0.0.4" } }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, + "sum-up": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", + "integrity": "sha1-HGYfZnBX9jvLeHWqFDi8FiUlFW4=", + "requires": { + "chalk": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "token-stream": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "requires": { + "glob": "^7.1.2" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -842,6 +3593,46 @@ "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", "optional": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -852,21 +3643,130 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -886,6 +3786,35 @@ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", @@ -896,6 +3825,21 @@ "decamelize": "^1.0.0", "window-size": "0.1.0" } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + } + } } } } diff --git a/package.json b/package.json index 9fb1f5f..627a90c 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,16 @@ "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", + "express-compile-sass": "latest", + "express-graphql": "^0.8.0", + "express-session": "latest", "fs-extra": "^7.0.1", + "graphql": "^14.3.0", + "graphql-import": "^0.7.1", "http-errors": "~1.6.3", + "js-yaml": "latest", "morgan": "~1.9.1", - "pug": "2.0.0-beta11", - "express-compile-sass": "latest", - "express-session": "latest", - "js-yaml": "latest" + "node-sass": "^4.12.0", + "pug": "2.0.0-beta11" } } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 4acfa78..fa95dac 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -14,10 +14,15 @@ async function submitBingoWords() { } async function submitUsername() { - let username = document.querySelector('#username-input').value; + let unameInput = document.querySelector('#username-input'); + let username = unameInput.value; let response = await postLocData({ username: username }); + unameInput.value = ''; + unameInput.placeholder = username; + document.querySelector('#username-form').remove(); + document.querySelector('.greyover').remove(); console.log(response); } @@ -31,12 +36,15 @@ async function submitWord(word) { let data = JSON.parse(response.data); for (let row of data.fieldGrid) { for (let field of row) { - document.querySelector(`.bingo-word-panel[b-word="${field.word}"]`) - .setAttribute('b-sub', field.submitted); + document.querySelectorAll(`.bingo-word-panel[b-word="${field.base64Word}"]`).forEach(x => { + x.setAttribute('b-sub', field.submitted); + }); } } if (data.bingo) { document.querySelector('#bingo-button').setAttribute('class', ''); + } else { + document.querySelector('#bingo-button').setAttribute('class', 'hidden'); } } @@ -85,4 +93,4 @@ window.onload = () => { gridSizeElem.oninput = () => { document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; }; -}; \ No newline at end of file +}; diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 1761747..50631ed 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -9,14 +9,43 @@ textarea display: block margin: 1rem border-radius: 0 - height: 50% - width: 50% - font-size: 15pt + font-size: 0.8em + +@media(max-device-width: 641px) + textarea + height: 80% + width: calc(100% - 2rem) + #words-container + width: 100% + height: 80% + +@media(min-device-width: 641px) + textarea + height: 80% + width: 50% + #words-container + width: 100% + height: 88% .number-input width: 4rem margin: 1rem +#bingoheader + display: table + width: 100% + + div + display: table-cell + text-align: start + + .stretchDiv + text-align: end + + button + max-width: calc(100% - 2rem) + padding: 0.7rem 2rem + #words-container display: table @@ -26,9 +55,18 @@ textarea .bingo-word-panel @include default-element display: table-cell - padding: 3rem + padding: 1rem transition-duration: 0.3s max-width: 15rem + border-radius: 0 + border-collapse: collapse + text-align: center + vertical-align: middle + + span + vertical-align: middle + display: inline-block + word-break: break-word .bingo-word-panel:hover background-color: darken($primary, 2%) @@ -40,6 +78,32 @@ textarea .bingo-word-panel[b-sub="true"] background-color: forestgreen +#bingo-button + transition-duration: 0.8s + +#username-form + @include default-element + position: fixed + display: block + height: calc(50% - 1rem) + width: calc(40% - 1rem) + top: 25% + left: 30% + text-align: center + vertical-align: middle + padding: 1rem + z-index: 1000 + + button + cursor: pointer + + input[type='text'] + cursor: text + +#username-form * + display: inline-block + vertical-align: middle + .popup @include default-element height: 5% @@ -59,4 +123,4 @@ textarea z-index: 99 top: 0 left: 0 - background-color: transparentize($primary, 0.5) \ No newline at end of file + background-color: transparentize($primary, 0.5) diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass index 82a0018..af8b73b 100644 --- a/public/stylesheets/sass/mixins.sass +++ b/public/stylesheets/sass/mixins.sass @@ -4,4 +4,5 @@ background: lighten($primary, 10%) color: $primarySurface border: 2px solid $primarySurface - border-radius: $borderRadius \ No newline at end of file + border-radius: $borderRadius + transition-duration: 0.2s diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 653cfd1..8b14e00 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -2,16 +2,39 @@ @import classes @import mixins +@media (min-device-width: 320px) + html + font-size: 4.5vw + +@media (min-device-width: 481px) + html + font-size: 4vw + +@media (min-device-width: 641px) + html + font-size: 4vw + +@media (min-device-width: 961px) + html + font-size: 3vw + +@media (min-device-width: 1025px) + html + font-size: 2vw + +@media (min-device-width: 1281px) + html + font-size: 1.5vw + body background-color: $primary color: $primarySurface - font-size: 18pt font-family: Arial, sans-serif button @include default-element - font-size: 20pt - padding: 10px + font-size: 1.2rem + padding: 0.7rem transition-duration: 0.2s button:hover @@ -23,6 +46,6 @@ button:active input @include default-element - font-size: 20pt + font-size: 1.2rem background-color: lighten($primary, 10%) - padding: 9px \ No newline at end of file + padding: 0.7rem diff --git a/routes/bingo.js b/routes/bingo.js index 684fb17..c370e5e 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -30,29 +30,55 @@ class BingoSession { let id = user.id; this.users[id] = user; } + + /** + * Graphql endpoint + * @param args {Object} - the arguments passed on the graphql interface + * @returns {any[]|*} + */ + players(args) { + if (args.id) + return [this.users[args.id]]; + else + return Object.values(this.users); + } } class BingoUser { + /** + * Bingo User class to store user information + */ constructor() { this.id = generateBingoId(); this.game = null; this.username = 'anonymous'; this.grids = {}; - this.submittedWords = {}; } } class BingoWordField { + /** + * Represents a single bingo field with the word an the status. + * It also holds the base64-encoded word. + * @param word + */ constructor(word) { this.word = word; + this.base64Word = Buffer.from(word).toString('base64'); this.submitted = false; } } class BingoGrid { + /** + * Represents the bingo grid containing all the words. + * @param wordGrid + * @returns {BingoGrid} + */ constructor(wordGrid) { this.wordGrid = wordGrid; this.fieldGrid = wordGrid.map(x => x.map(y => new BingoWordField(y))); + this.bingo = false; return this; } } @@ -74,6 +100,20 @@ function shuffleArray(array) { return array; } +/** + * Inflates an array to a minimum Size + * @param array {Array} - the array to inflate + * @param minSize {Number} - the minimum size that the array needs to have + * @returns {Array} + */ +function inflateArray(array, minSize) { + let resultArray = array; + let iterations = Math.ceil(minSize/array.length); + for (let i = 0; i < iterations; i++) + resultArray = [...resultArray, ...resultArray]; + return resultArray +} + /** * Generates an id for a subreddit download. * @returns {string} @@ -89,7 +129,7 @@ function generateBingoId() { * @returns {BingoGrid} */ function generateWordGrid(dimensions, words) { - let shuffledWords = shuffleArray(words); + let shuffledWords = shuffleArray(inflateArray(words, dimensions[0]*dimensions[1])); let grid = []; for (let x = 0; x < dimensions[1]; x++) { grid[x] = []; @@ -102,19 +142,17 @@ function generateWordGrid(dimensions, words) { /** * Sets the submitted parameter of the words in the bingo grid that match to true. - * @param word {String} - * @param bingoGrid {BingoGrid} + * @param base64Word {String} - base64 encoded bingo word + * @param bingoGrid {BingoGrid} - the grid where the words are stored * @returns {boolean} */ -function submitWord(word, bingoGrid) { - let results = bingoGrid.fieldGrid.find(x => x.find(y => (y.word === word))).find(x => x.word === word); - - if (results) { - (results instanceof Array)? results.forEach(x => {x.submitted = true}): results.submitted = true; - checkBingo(bingoGrid); - return true; - } - return false; +function toggleHeared(base64Word, bingoGrid) { + for (let row of bingoGrid.fieldGrid) + for (let field of row) + if (base64Word === field.base64Word) + field.submitted = !field.submitted; + checkBingo(bingoGrid); + return true; } /** @@ -146,8 +184,10 @@ function checkBingo(bingoGrid) { bingoCheck = true; for (let field of row) bingoCheck = field && bingoCheck; - if (bingoCheck) - break; + if (bingoCheck) { + bingoGrid.bingo = true; + return true; + } } if (bingoCheck) { bingoGrid.bingo = true; @@ -159,13 +199,16 @@ function checkBingo(bingoGrid) { bingoCheck = true; for (let j = 0; j < fg.length; j++) bingoCheck = fg[j][i] && bingoCheck; - if (bingoCheck) - break; + if (bingoCheck) { + bingoGrid.bingo = true; + return true; + } } if (bingoCheck) { bingoGrid.bingo = true; return true; } + bingoGrid.bingo = false; return false; } @@ -191,7 +234,7 @@ 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].wordGrid, username: bingoUser.username}); + res.render('bingo/bingo-game', {grid: bingoUser.grids[gameId].fieldGrid, username: bingoUser.username}); } else { res.render('bingo/bingo-submit'); } @@ -226,12 +269,9 @@ router.post('/', (req, res) => { } else if (data.game) { res.send(bingoSessions[data.game]); } else if (data.bingoWord) { - if (!bingoUser.submittedWords[gameId]) - bingoUser.submittedWords[gameId] = []; - bingoUser.submittedWords[gameId].push(data.bingoWord); console.log(typeof bingoUser.grids[gameId]); if (bingoUser.grids[gameId]) - submitWord(data.bingoWord, bingoUser.grids[gameId]); + toggleHeared(data.bingoWord, bingoUser.grids[gameId]); res.send(bingoUser.grids[gameId]); } else if (data.bingo) { if (checkBingo(bingoUser.grids[gameId])) { @@ -239,7 +279,7 @@ router.post('/', (req, res) => { bingoSession.bingos.push(bingoUser.id); bingoSession.finished = true; setTimeout(() => { // delete the finished game after five minutes - delete bingoSessions[game.id]; + delete bingoSessions[gameId]; }, 360000); res.send(bingoSession); } else { @@ -256,4 +296,68 @@ router.post('/', (req, res) => { } }); +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]; + else + return bingoSession; + }, + checkBingo: (args) => { + return checkBingo(bingoUser.grids[gameId]) + }, + activeGrid: (args) => { + return bingoUser.grids[gameId]; + }, + // mutation + createGame: (args) => { + let words = args.words; + let size = args.size; + let game = new BingoSession(words, size); + + bingoSessions[game.id] = game; + + setTimeout(() => { // delete the game after one day + delete bingoSessions[game.id]; + }, 86400000); + + return game; + }, + submitBingo: (args) => { + 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; + } else { + return false; + } + }, + toggleWord: (args) => { + if (args.word || args.base64Word) { + args.base64Word = args.base64Word || Buffer.from(args.word).toString('base-64'); + if (bingoUser.grids[gameId]) + toggleHeared(args.base64Word, bingoUser.grids[gameId]); + return bingoUser.grids[gameId]; + } + }, + setUsername: (args) => { + if (args.username) { + bingoUser.username = args.username; + bingoSession.addUser(bingoUser); + + return bingoUser; + } + } + }; +}; + module.exports = router; diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index d0e2f31..da29761 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -1,13 +1,15 @@ include bingo-layout block content - div(id='username-form') - input(type='text', id='username-input', placeholder='username', value=username) - button(onclick='submitUsername()') Set Username - button(id='bingo-button' onclick='submitBingo()', class='hidden') Bingo! + if username === 'anonymous' + div(class='greyover') + div(id='username-form') + 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 word in val - div(class='bingo-word-panel', onclick=`submitWord('${word}')`, b-word=word, b-sub='false') - span= word \ No newline at end of file + 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! diff --git a/views/bingo/bingo-layout.pug b/views/bingo/bingo-layout.pug index 7f6f4a4..b3191e6 100644 --- a/views/bingo/bingo-layout.pug +++ b/views/bingo/bingo-layout.pug @@ -3,6 +3,5 @@ html include ../includes/head script(type='text/javascript', src='/javascripts/bingo-web.js') link(rel='stylesheet', href='/sass/bingo/style.sass') - body - block content \ No newline at end of file + block content diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug index 577cc1e..9a60b0a 100644 --- a/views/bingo/bingo-submit.pug +++ b/views/bingo/bingo-submit.pug @@ -2,8 +2,11 @@ extends bingo-layout block content div(id='bingoform') - input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8) - span x - span(id='bingo-grid-y', class='number-input') 3 - button(onclick='submitBingoWords()') Submit - textarea(id='bingo-textarea', placeholder='Bingo Words') \ No newline at end of file + div(id='bingoheader') + div + input(type='number', id='bingo-grid-size', class='number-input', value=3, min=1, max=8) + span x + span(id='bingo-grid-y', class='number-input') 3 + div(class='stretchDiv') + button(onclick='submitBingoWords()') Submit + textarea(id='bingo-textarea', placeholder='Bingo Words') From 02fd2665f2db6dc968234c22708123cb19b3d790 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 14:12:44 +0200 Subject: [PATCH 05/42] 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 --- graphql/bingo.graphql | 43 ++++- public/javascripts/bingo-web.js | 235 ++++++++++++++++++----- public/javascripts/common.js | 33 +++- public/stylesheets/sass/bingo/style.sass | 32 ++- public/stylesheets/sass/classes.sass | 4 +- routes/bingo.js | 108 +++-------- views/bingo/bingo-game.pug | 24 ++- views/bingo/bingo-submit.pug | 2 +- 8 files changed, 336 insertions(+), 145 deletions(-) diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 7270897..82db62c 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -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 } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index fa95dac..c891bc1 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -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} + */ 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} + */ 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} + */ 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} + */ 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} + */ 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 = `${player.username}`; + 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; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index 0883713..15840a7 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -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('&'); -} \ No newline at end of file +} diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 50631ed..7c745f6 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -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% diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 0a6474d..4dffa00 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -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% \ No newline at end of file + left: 30% diff --git a/routes/bingo.js b/routes/bingo.js index c370e5e..4b394ac 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -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; } diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index da29761..a0d3bdf 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -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! diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug index 9a60b0a..2a5228d 100644 --- a/views/bingo/bingo-submit.pug +++ b/views/bingo/bingo-submit.pug @@ -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') From c98c57cfa256773f65a708098bef935a156fb16e Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 14:25:55 +0200 Subject: [PATCH 06/42] bin/www now reads config - added reading the yaml config to bin/www to configure the port --- bin/www | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/bin/www b/bin/www index 8c61406..49c2891 100644 --- a/bin/www +++ b/bin/www @@ -4,22 +4,33 @@ * Module dependencies. */ -var app = require('../app'); -var debug = require('debug')('whooshy:server'); -var http = require('http'); +const app = require('../app'); +const debug = require('debug')('whooshy:server'); +const http = require('http'); +const yaml = require('js-yaml'); +const fsx = require('fs-extra'); + +try { + let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); + + if (fsx.existsSync('config.yaml')) + Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); +} catch (err) { + console.error(err); +} /** * Get port from environment and store in Express. */ -var port = normalizePort(process.env.PORT || '3000'); +let port = normalizePort(process.env.PORT || settings.port || '3000'); app.set('port', port); /** * Create HTTP server. */ -var server = http.createServer(app); +let server = http.createServer(app); /** * Listen on provided port, on all network interfaces. @@ -34,7 +45,7 @@ server.on('listening', onListening); */ function normalizePort(val) { - var port = parseInt(val, 10); + let port = parseInt(val, 10); if (isNaN(port)) { // named pipe @@ -58,7 +69,7 @@ function onError(error) { throw error; } - var bind = typeof port === 'string' + let bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; @@ -82,8 +93,8 @@ function onError(error) { */ function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' + let addr = server.address(); + let bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); From 51a22eca86912e167e9f0f5233c4540e4c82c751 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 14:28:29 +0200 Subject: [PATCH 07/42] Hotfix settings not defined - added preinitialized settings to bin/www --- bin/www | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/www b/bin/www index 49c2891..7f43cbf 100644 --- a/bin/www +++ b/bin/www @@ -10,8 +10,10 @@ const http = require('http'); const yaml = require('js-yaml'); const fsx = require('fs-extra'); +let settings = {}; + try { - let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); + settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); if (fsx.existsSync('config.yaml')) Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); From 68faea7d86032694df3d390a7379089387a39013 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 14:32:29 +0200 Subject: [PATCH 08/42] Hotfix: Missing game urlparam in front-end - added default return value if the game param is not set to getGameParam on bingo --- public/javascripts/bingo-web.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index c891bc1..c9ee9f6 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -3,7 +3,11 @@ * @returns {string} */ function getGameParam() { - return window.location.href.match(/\?game=(\w+)/)[1]; + let matches = window.location.href.match(/\?game=(\w+)/); + if (matches) + return matches[1]; + else + return ''; } /** From 4d87c70b695f5200da3fb6bf256dcb3f3b50e0c9 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 18:14:55 +0200 Subject: [PATCH 09/42] Redesign and Rematch - added the option to leave or play again after a match - added error messages - redesigned with css-grids --- app.js | 8 +- graphql/bingo.graphql | 6 ++ public/javascripts/bingo-web.js | 111 ++++++++++++++++------- public/stylesheets/sass/bingo/style.sass | 107 +++++++++++++++++----- public/stylesheets/sass/classes.sass | 6 ++ public/stylesheets/sass/style.sass | 10 +- public/stylesheets/sass/vars.sass | 6 +- routes/bingo.js | 59 +++++++++--- views/bingo/bingo-game.pug | 24 ++--- views/bingo/bingo-layout.pug | 1 + views/bingo/bingo-submit.pug | 3 +- views/index.pug | 1 + views/layout.pug | 2 +- 13 files changed, 251 insertions(+), 93 deletions(-) diff --git a/app.js b/app.js index 6fa7cdd..af39593 100644 --- a/app.js +++ b/app.js @@ -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 }; diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 82db62c..43e0d79 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -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 { diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index c9ee9f6..882af3a 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -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} */ 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} + */ +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 = ` +

${name} has won!

+ + + `; 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 = `${errorMessage}`; + 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:`; }; }; diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 7c745f6..d79b6c2 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -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) diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 4dffa00..2b4d91a 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -11,3 +11,9 @@ position: fixed top: 20% left: 30% + +.grid + display: grid + +.inline-grid + display: inline-grid diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 8b14e00..066a693 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -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%) diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass index 9f8b9b2..365b26d 100644 --- a/public/stylesheets/sass/vars.sass +++ b/public/stylesheets/sass/vars.sass @@ -1,4 +1,6 @@ $primary: #223 $primarySurface: white - -$borderRadius: 20px \ No newline at end of file +$secondary: teal +$borderRadius: 20px +$inactive: #aaa +$error: #a00 diff --git a/routes/bingo.js b/routes/bingo.js index 4b394ac..ec1cb0a 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -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); + } } }; }; diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index a0d3bdf..0cffc18 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -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! diff --git a/views/bingo/bingo-layout.pug b/views/bingo/bingo-layout.pug index b3191e6..9549b96 100644 --- a/views/bingo/bingo-layout.pug +++ b/views/bingo/bingo-layout.pug @@ -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 diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug index 2a5228d..e351819 100644 --- a/views/bingo/bingo-submit.pug +++ b/views/bingo/bingo-submit.pug @@ -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') diff --git a/views/index.pug b/views/index.pug index 3d63b9a..8b93234 100644 --- a/views/index.pug +++ b/views/index.pug @@ -3,3 +3,4 @@ extends layout block content h1= title p Welcome to #{title} + button(onclick='window.location.href="/bingo"') Bingo diff --git a/views/layout.pug b/views/layout.pug index 15af079..6f767bf 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -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 From 3ac3180957834d13d481596fc746c036934007c2 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sat, 11 May 2019 21:22:35 +0200 Subject: [PATCH 10/42] CHANGELOG and README - added content to the README.md - added a CHANGELOG.md --- CHANGELOG.md | 12 ++++++++++++ README.md | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6689dee --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- CHANGELOG.md Changelog +- content to the README.md diff --git a/README.md b/README.md index bbf06df..26dab37 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ -whooshy +# whooshy + +This repository is the node.js webserver running on `(beta.)trivernis.net`. + +## Requirements + +- node.js 8 or higher +- npm +- the requirements of [reddit-riddle](https://github.com/trivernis/reddit-riddle) + +## Install + +After you have cloned the repository you can install it by executing the `install.sh` script. + +## Features + +Currently the webserver serves very few sites. It is still in an early stage of development. Included are: + +### Riddle-Web + +A webinterface for the [reddit-riddle](https://github.com/trivernis/reddit-riddle) python script that allows you to download images from reddit. The requested subreddit images will be downloaded to the server and then served to be downloaded by the client. + +### Bingo + +A simple bingo game where you can provide your own bingo words. You can then play it with your friends. The first one to score a bingo wins. + +## Planned + +- [ ] logging with `winston.js` +- [ ] login system with a postgres database +- [ ] social bord or blog (`markdown.it` formatted) +- [ ] memepage From 20e15401757bbbbdfbd147492c19d58f80c52b80 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 00:26:56 +0200 Subject: [PATCH 11/42] Added Bingo Chat - added a chat to the bingo game --- CHANGELOG.md | 1 + graphql/bingo.graphql | 118 +++++++++++++++++------ package-lock.json | 40 ++++++++ package.json | 2 + public/javascripts/bingo-web.js | 58 +++++++++++ public/stylesheets/sass/bingo/style.sass | 57 +++++++++-- public/stylesheets/sass/style.sass | 15 +++ routes/bingo.js | 71 +++++++++++++- views/bingo/bingo-game.pug | 3 + 9 files changed, 324 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6689dee..fae8712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CHANGELOG.md Changelog - content to the README.md +- Chat to the bingo game (renderd with markdown-it) diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 43e0d79..36ec33a 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -10,10 +10,13 @@ type BingoMutation { toggleWord(input: WordInput!): BingoGrid # set the username of the current session - setUsername(input: UsernameInput): BingoUser + setUsername(input: UsernameInput!): BingoUser # recreates the active game to a follow-up createFollowupGame: BingoGame + + # sends a message to the current sessions chat + sendChatMessage(input: MessageInput!): ChatMessage } type BingoQuery { @@ -28,36 +31,6 @@ 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 @@ -80,6 +53,9 @@ type BingoGame { # the id of the followup game if it has been created followup: ID + + # Returns the last n chat-messages + getMessages(input: MessageQueryInput): [ChatMessage!] } type BingoUser { @@ -117,3 +93,83 @@ type BingoField { # the base64 encoded word base64Word: String } + +type ChatMessage { + + # the id of the message + id: ID! + + # the content of the message + content: String! + + # the content of the message rendered by markdown-it + htmlContent: String + + # the type of the message + type: MessageType! + + # the username of the sender + username: String + + # the time the message was send (in milliseconds) + datetime: String! +} + +# # +# input Types # +# # + +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! +} + +input MessageInput { + + # the message + message: String! +} + +input MessageQueryInput { + + # search for a specific id + id: ID + + # get the last n messages + last: Int = 10 +} + +# # +# enum Types # +# # + +enum MessageType { + USER + ERROR + INFO +} diff --git a/package-lock.json b/package-lock.json index 389a636..e1f03ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -761,6 +761,11 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2158,6 +2163,14 @@ "invert-kv": "^1.0.0" } }, + "linkify-it": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", + "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -2216,6 +2229,28 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-emoji": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", + "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3570,6 +3605,11 @@ "mime-types": "~2.1.24" } }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index 627a90c..a17f1e6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "graphql-import": "^0.7.1", "http-errors": "~1.6.3", "js-yaml": "latest", + "markdown-it": "^8.4.2", + "markdown-it-emoji": "^1.4.0", "morgan": "~1.9.1", "node-sass": "^4.12.0", "pug": "2.0.0-beta11" diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 882af3a..c89e5ac 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -187,6 +187,12 @@ async function refresh() { username id } + getMessages { + id + username + type + htmlContent + } } } }`, null, `/graphql?game=${getGameParam()}`); @@ -213,6 +219,10 @@ async function refresh() { } } } + for (let chatMessage of bingoSession.getMessages) { + if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`)) + addChatMessage(chatMessage); + } } else { if (response.status === 400) clearInterval(refrInterval); @@ -260,6 +270,54 @@ function showError(errorMessage) { }, 10000); } +async function sendChatMessage() { + let messageInput = document.querySelector('#chat-input'); + if (messageInput.value && messageInput.value.length > 0) { + let message = messageInput.value; + let response = await postGraphqlQuery(` + mutation($message: String!) { + bingo { + sendChatMessage(input: { message: $message }) { + id + htmlContent + username + type + } + } + }`,{message: message}, `/graphql?game=${getGameParam()}`); + if (response.status === 200) { + addChatMessage(response.data.bingo.sendChatMessage); + messageInput.value = ''; + } else { + console.error(response); + showError('Error when sending message.'); + } + } +} + +/** + * Adds a message to the chat + * @param messageObject {Object} - the message object returned by graphql + */ +function addChatMessage(messageObject) { + let msgSpan = document.createElement('span'); + msgSpan.setAttribute('class', 'chatMessage'); + msgSpan.setAttribute('msg-id', messageObject.id); + if (messageObject.type === "USER") { + msgSpan.innerHTML = ` + ${messageObject.username}: + ${messageObject.htmlContent} + `; + } else { + msgSpan.innerHTML = ` + ${messageObject.htmlContent} + `; + } + let chatContent = document.querySelector('#chat-content'); + chatContent.appendChild(msgSpan); + chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom +} + /** * Executes the provided function if the key-event is an ENTER-key * @param event {Event} - the generated key event diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index d79b6c2..ab28468 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -22,6 +22,7 @@ textarea #content-container grid-template-columns: 0 100% !important grid-template-rows: 10% 80% 10% !important + #players-container div display: none padding: 0 @@ -126,11 +127,14 @@ textarea #content-container display: grid - grid-template-columns: 20% 80% - grid-template-rows: 10% 80% 10% + grid-template-columns: 25% 75% + grid-template-rows: 10% 40% 40% 10% height: 100% width: 100% + div + overflow: auto + #button-container grid-column-start: 1 grid-column-end: 1 @@ -150,28 +154,63 @@ textarea grid-column-start: 1 grid-column-end: 1 grid-row-start: 2 - grid-row-end: 2 + grid-row-end: 3 h1 margin: 0 0 1rem 0 #words-container grid-column-start: 2 - grid-column-end: 2 + grid-column-end: 3 grid-row-start: 2 - grid-row-end: 2 + grid-row-end: 4 + + #chat-container + grid-column-start: 1 + grid-column-end: 1 + grid-row-start: 3 + grid-row-end: 4 + height: 100% + border: 1px solid $inactive + margin: 0 0.5rem + + #chat-content + height: calc(100% - 3.5rem) + background-color: $primary + overflow: auto + + .chatMessage + display: list-item + padding: 0.2rem + + .chatUsername + color: $inactive + + .ERROR + color: $error + + .INFO + color: $inactive + font-style: italic + + #chat-input + width: 100% + margin: 1rem 0 0 0 + height: 2.5rem + border-radius: 0 .errorDiv grid-column-start: 2 - grid-column-end: 2 - grid-row-start: 3 - grid-row-end: 3 + grid-column-end: 3 + grid-row-start: 4 + grid-row-end: 4 background-color: $error text-align: center margin: 0.75rem 0 border-radius: 1rem height: calc(100% - 1.5rem) display: table + span display: table-cell font-size: 1.8rem @@ -210,4 +249,4 @@ textarea z-index: 99 top: 0 left: 0 - background-color: rgba(0,0,0,0.5) + background-color: rgba(0, 0, 0, 0.5) diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 066a693..3b56a96 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -53,3 +53,18 @@ input textarea background-color: lighten($primary, 15%) + +a + color: $secondary + +::-webkit-scrollbar + width: 12px + height: 12px + +::-webkit-scrollbar-thumb + background: darken($secondary, 5) + border-radius: 10px + +::-webkit-scrollbar-track + background: lighten($primary, 5) + border-radius: 10px diff --git a/routes/bingo.js b/routes/bingo.js index ec1cb0a..3daa8b1 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1,7 +1,9 @@ const express = require('express'), router = express.Router(), cproc = require('child_process'), - fsx = require('fs-extra'); + fsx = require('fs-extra'), + mdEmoji = require('markdown-it-emoji'), + md = require('markdown-it')().use(mdEmoji); const rWordOnly = /^\w+$/; @@ -21,6 +23,7 @@ class BingoSession { this.bingos = []; // array with the users that already had bingo this.finished = false; this.followup = null; + this.chatMessages = []; } /** @@ -53,8 +56,54 @@ class BingoSession { let followup = new BingoSession(this.words, this.gridSize); this.followup = followup.id; bingoSessions[followup.id] = followup; + followup.chatMessages = this.chatMessages; + followup.chatMessages.push(new BingoChatMessage('--- Rematch ---', "INFO")); return followup; } + + /** + * Graphql endpoint to get the last n messages or messages by id + * @param args {Object} - arguments passed by graphql + * @returns {[]} + */ + getMessages(args) { + let input = args.input || null; + if (input && input.id) { + return this.chatMessages.find(x => (x && x.id === input.id)); + } else if (input && input.last) { + return this.chatMessages.slice(-input.last); + } else { + return this.chatMessages.slice(-10); + } + } + + /** + * Sends the message that a user toggled a word. + * @param base64Word + * @param bingoUser + */ + sendToggleInfo(base64Word, bingoUser) { + let word = Buffer.from(base64Word, 'base64').toString(); + let toggleMessage = new BingoChatMessage(`${bingoUser.username} toggled phrase "${word}"`, "INFO"); + this.chatMessages.push(toggleMessage); + } +} + +class BingoChatMessage { + /** + * Chat Message class constructor + * @param messageContent {String} - the messages contents + * @param type {String} - the type constant of the message (USER, ERROR, INFO) + * @param [username] {String} - the username of the user who send this message + */ + constructor(messageContent, type="USER", username) { + this.id = generateBingoId(); + this.content = messageContent; + this.htmlContent = md.renderInline(messageContent); + this.datetime = Date.now(); + this.username = username; + this.type = type; + } } class BingoUser { @@ -96,6 +145,15 @@ class BingoGrid { } } +/** + * Replaces tag signs with html-escaped signs. + * @param htmlString + * @returns {string} + */ +function replaceTagSigns(htmlString) { + return htmlString.replace(//g, '>'); +} + /** * Shuffles the elements in an array * @param array {Array<*>} @@ -316,6 +374,7 @@ router.graphqlResolver = (req, res) => { input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); if (bingoUser.grids[gameId]) { toggleHeared(input.base64Word, bingoUser.grids[gameId]); + bingoSession.sendToggleInfo(input.base64Word, bingoUser); return bingoUser.grids[gameId]; } else { res.status(400); @@ -343,6 +402,16 @@ router.graphqlResolver = (req, res) => { } else { res.status(400); } + }, + sendChatMessage: ({input}) => { + input.message = replaceTagSigns(input.message); + if (bingoSession && input.message) { + let userMessage = new BingoChatMessage(input.message, 'USER', bingoUser.username); + bingoSession.chatMessages.push(userMessage); + return userMessage; + } else { + res.status(400); + } } }; }; diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index 0cffc18..78654b6 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -13,6 +13,9 @@ block content 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)') div(id='words-container') each val in grid div(class='bingo-word-row') From 96425d563adda115cbdfe096f097e637bc3efedc Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 01:10:22 +0200 Subject: [PATCH 12/42] Improved backend security - added max values for all bingo graphql endpoints - added sqlite3 session-store with `connect-sqlite3` --- .gitignore | 2 + app.js | 2 + package-lock.json | 185 +++++++++++++++++++++++ package.json | 1 + public/javascripts/bingo-web.js | 34 +++-- public/stylesheets/sass/bingo/style.sass | 8 +- routes/bingo.js | 10 +- views/bingo/bingo-game.pug | 4 +- views/bingo/bingo-submit.pug | 2 +- 9 files changed, 225 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index adda588..34d79e9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ node_modules scripts/* tmp config.yaml +sessions +sessions-journal diff --git a/app.js b/app.js index af39593..a4c080f 100644 --- a/app.js +++ b/app.js @@ -5,6 +5,7 @@ const createError = require('http-errors'), logger = require('morgan'), compileSass = require('express-compile-sass'), session = require('express-session'), + SQLiteStore = require('connect-sqlite3')(session), fsx = require('fs-extra'), yaml = require('js-yaml'), graphqlHTTP = require('express-graphql'), @@ -39,6 +40,7 @@ app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(session({ + store: new SQLiteStore, secret: settings.sessions.secret, resave: false, saveUninitialized: true, diff --git a/package-lock.json b/package-lock.json index e1f03ba..2621680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -474,6 +474,11 @@ "upath": "^1.1.1" } }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -567,6 +572,14 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "connect-sqlite3": { + "version": "0.9.11", + "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.11.tgz", + "integrity": "sha1-TlQVXcLq3ypZmDhbLzXoPdkkCy0=", + "requires": { + "sqlite3": "^4.0.0" + } + }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -675,6 +688,11 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -737,6 +755,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -1131,6 +1154,14 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "^2.2.1" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1832,6 +1863,14 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, "in-publish": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", @@ -1859,6 +1898,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, "inline-source-map-comment": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz", @@ -2341,6 +2385,30 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "requires": { + "minipass": "^2.2.1" + } + }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", @@ -2422,6 +2490,31 @@ } } }, + "needle": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz", + "integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==", + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -2453,6 +2546,53 @@ } } }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, "node-sass": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", @@ -2525,6 +2665,20 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" + }, + "npm-packlist": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -2896,6 +3050,17 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -3127,6 +3292,11 @@ } } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -3371,6 +3541,16 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "sqlite3": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.8.tgz", + "integrity": "sha512-kgwHu4j10KhpCHtx//dejd/tVQot7jc3sw+Sn0vMuKOw0X00Ckyg9VceKgzPyGmmz+zEoYue9tOLriWTvYy0ww==", + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.11.0", + "request": "^2.87.0" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -3461,6 +3641,11 @@ "get-stdin": "^4.0.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, "sum-up": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", diff --git a/package.json b/package.json index a17f1e6..176f489 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "start": "node ./bin/www" }, "dependencies": { + "connect-sqlite3": "^0.9.11", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index c89e5ac..59179f4 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -76,8 +76,9 @@ async function createFollowup() { */ async function submitUsername() { let unameInput = document.querySelector('#username-input'); - let username = unameInput.value; - let response = await postGraphqlQuery(` + let username = unameInput.value.replace(/^\s+|\s+$/g, ''); + if (username.length > 1 && username !== 'anonymous') { + let response = await postGraphqlQuery(` mutation($username:String!) { bingo { setUsername(input: {username: $username}) { @@ -86,16 +87,19 @@ async function submitUsername() { } } }`, { - username: username - },`/graphql?game=${getGameParam()}`); - if (response.status === 200) { - unameInput.value = ''; - unameInput.placeholder = response.data.username; - document.querySelector('#username-form').remove(); - document.querySelector('.greyover').remove(); + username: username + },`/graphql?game=${getGameParam()}`); + if (response.status === 200) { + unameInput.value = ''; + unameInput.placeholder = response.data.username; + document.querySelector('#username-form').remove(); + document.querySelector('.greyover').remove(); + } else { + showError(`Failed to submit username. HTTP Error: ${response.status}`); + console.error(response); + } } else { - showError(`Failed to submit username. HTTP Error: ${response.status}`); - console.error(response); + showError('You need to provide a username (minimum 2 characters)!'); } } @@ -306,12 +310,10 @@ function addChatMessage(messageObject) { if (messageObject.type === "USER") { msgSpan.innerHTML = ` ${messageObject.username}: - ${messageObject.htmlContent} - `; + ${messageObject.htmlContent}`; } else { msgSpan.innerHTML = ` - ${messageObject.htmlContent} - `; + ${messageObject.htmlContent}`; } let chatContent = document.querySelector('#chat-content'); chatContent.appendChild(msgSpan); @@ -334,6 +336,8 @@ window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { }); window.onload = () => { + if (document.querySelector('#chat-container')) + refresh(); if (window && !document.querySelector('#bingoform')) { refrInterval = setInterval(refresh, 1000); // global variable to clear } diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index ab28468..c5b0c92 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -23,14 +23,17 @@ textarea grid-template-columns: 0 100% !important grid-template-rows: 10% 80% 10% !important - #players-container div - display: none + #players-container #chat-container + display: none !important 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 @@ -173,6 +176,7 @@ textarea height: 100% border: 1px solid $inactive margin: 0 0.5rem + word-break: break-word #chat-content height: calc(100% - 3.5rem) diff --git a/routes/bingo.js b/routes/bingo.js index 3daa8b1..b14b677 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -33,6 +33,9 @@ class BingoSession { addUser(user) { let id = user.id; this.users[id] = user; + if (user.username !== 'anonymous') { + this.chatMessages.push(new BingoChatMessage(`**${user.username}** joined.`, "INFO")); + } } /** @@ -57,7 +60,7 @@ class BingoSession { this.followup = followup.id; bingoSessions[followup.id] = followup; followup.chatMessages = this.chatMessages; - followup.chatMessages.push(new BingoChatMessage('--- Rematch ---', "INFO")); + followup.chatMessages.push(new BingoChatMessage('**Rematch**', "INFO")); return followup; } @@ -84,7 +87,7 @@ class BingoSession { */ sendToggleInfo(base64Word, bingoUser) { let word = Buffer.from(base64Word, 'base64').toString(); - let toggleMessage = new BingoChatMessage(`${bingoUser.username} toggled phrase "${word}"`, "INFO"); + let toggleMessage = new BingoChatMessage(`**${bingoUser.username}** toggled phrase "${word}".`, "INFO"); this.chatMessages.push(toggleMessage); } } @@ -343,6 +346,7 @@ router.graphqlResolver = (req, res) => { }); let size = input.size; if (words.length > 0 && size < 10 && size > 0) { + words = words.slice(0, 10000); // only allow up to 10000 words in the bingo let game = new BingoSession(words, size); bingoSessions[game.id] = game; @@ -404,7 +408,7 @@ router.graphqlResolver = (req, res) => { } }, sendChatMessage: ({input}) => { - input.message = replaceTagSigns(input.message); + input.message = replaceTagSigns(input.message).substring(0, 250); if (bingoSession && input.message) { let userMessage = new BingoChatMessage(input.message, 'USER', bingoUser.username); bingoSession.chatMessages.push(userMessage); diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index 78654b6..1f9974a 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -4,7 +4,7 @@ block content if username === 'anonymous' div(class='greyover') div(id='username-form', onkeypress='submitOnEnter(event, submitUsername)') - input(type='text', id='username-input', placeholder=username) + input(type='text', id='username-input', placeholder=username, maxlength="30") span Maximum is 30 characters. button(onclick='submitUsername()') Set Username div(id='content-container') @@ -15,7 +15,7 @@ block content 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)') + 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') diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug index e351819..db8c533 100644 --- a/views/bingo/bingo-submit.pug +++ b/views/bingo/bingo-submit.pug @@ -10,4 +10,4 @@ block content div(class='stretchDiv') button(onclick='submitBingoWords()') Submit span(id='word-count') Please provide at least 9 phrases: - textarea(id='bingo-textarea', placeholder='Bingo Words') + textarea(id='bingo-textarea', placeholder='Bingo Words (max 10,000)', maxlength=1000000) From fc0e6c23c1eaf25d9e1171233da4e0e29b53213c Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 01:18:39 +0200 Subject: [PATCH 13/42] Fixed style - removed chat and players from mobile version --- public/stylesheets/sass/bingo/style.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index c5b0c92..07fb479 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -23,7 +23,7 @@ textarea grid-template-columns: 0 100% !important grid-template-rows: 10% 80% 10% !important - #players-container #chat-container + #players-container, #chat-container display: none !important padding: 0 From bd5d4d7e0e7e39371cf6e0b67452e964f6364d78 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 12 May 2019 00:54:53 +0000 Subject: [PATCH 14/42] Pin dependencies --- package-lock.json | 41 ++++++++++++++++++++++++++++++----------- package.json | 20 ++++++++++---------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 389a636..594a9b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1148,7 +1148,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1166,11 +1167,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1183,15 +1186,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1294,7 +1300,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1304,6 +1311,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1316,17 +1324,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1343,6 +1354,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -1415,7 +1427,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -1425,6 +1438,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -1500,7 +1514,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -1530,6 +1545,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -1547,6 +1563,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1585,11 +1602,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, diff --git a/package.json b/package.json index 627a90c..5045c6d 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,19 @@ "start": "node ./bin/www" }, "dependencies": { - "cookie-parser": "~1.4.4", - "debug": "~2.6.9", - "express": "~4.16.1", + "cookie-parser": "1.4.4", + "debug": "2.6.9", + "express": "4.16.4", "express-compile-sass": "latest", - "express-graphql": "^0.8.0", + "express-graphql": "0.8.0", "express-session": "latest", - "fs-extra": "^7.0.1", - "graphql": "^14.3.0", - "graphql-import": "^0.7.1", - "http-errors": "~1.6.3", + "fs-extra": "7.0.1", + "graphql": "14.3.0", + "graphql-import": "0.7.1", + "http-errors": "1.6.3", "js-yaml": "latest", - "morgan": "~1.9.1", - "node-sass": "^4.12.0", + "morgan": "1.9.1", + "node-sass": "4.12.0", "pug": "2.0.0-beta11" } } From 2dff089ca06131af4758a18b3082ea9488ef03a3 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 12:54:49 +0200 Subject: [PATCH 15/42] Switched to postgres for session storage - improved mobile layout (chat now visible) - fixed bingo button not visible in mobile layout - added postgres for session storage - added sql directory for sql query files --- CHANGELOG.md | 17 +++ app.js | 126 +++++++++++++---------- bin/www | 40 ++++--- default-config.yaml | 9 +- package-lock.json | 110 ++++++++++++++++++++ package.json | 3 +- public/javascripts/bingo-web.js | 11 ++ public/stylesheets/sass/bingo/style.sass | 39 ++++++- public/stylesheets/sass/mixins.sass | 1 - public/stylesheets/sass/style.sass | 10 +- sql/createSessionTable.sql | 7 ++ views/bingo/bingo-game.pug | 4 +- 12 files changed, 295 insertions(+), 82 deletions(-) create mode 100644 sql/createSessionTable.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index fae8712..307231e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + ### Added - CHANGELOG.md Changelog - content to the README.md - Chat to the bingo game (renderd with markdown-it) +- Postgres session storage +- sql-file directory `sql` + + +## Changed + +- changed export of `app.js` to the asynchronous init function that returns the app object +- `bin/www` now calls the init function of `app.js` + +### Removed + +- sqlite3 sesssion storage + +### Fixed + +- mobile layout diff --git a/app.js b/app.js index a4c080f..a6b7c73 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,8 @@ const createError = require('http-errors'), logger = require('morgan'), compileSass = require('express-compile-sass'), session = require('express-session'), - SQLiteStore = require('connect-sqlite3')(session), + pg = require('pg'), + pgSession = require('connect-pg-simple')(session), fsx = require('fs-extra'), yaml = require('js-yaml'), graphqlHTTP = require('express-graphql'), @@ -22,69 +23,86 @@ 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, response) => { - return { +async function init() { + // grapql default resolver + let graphqlResolver = (request, response) => { + return { time: Date.now(), bingo: bingoRouter.graphqlResolver(request, response) - } -}; -let app = express(); + } + }; + // database setup + let pgPool = new pg.Pool({ + host: settings.postgres.host, + port: settings.postgres.port, + user: settings.postgres.user, + password: settings.postgres.password, + database: settings.postgres.database + }); + await pgPool.query(fsx.readFileSync('./sql/createSessionTable.sql', 'utf-8')); -// view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'pug'); -app.set('trust proxy', 1); + let app = express(); -app.use(logger('dev')); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(session({ - store: new SQLiteStore, - secret: settings.sessions.secret, - resave: false, - saveUninitialized: true, - cookie: { - expires: 10000000 - } -})); -app.use('/sass', compileSass({ - root: './public/stylesheets/sass', - sourceMap: true, - watchFiles: true, - logToConsole: true -})); -app.use(express.static(path.join(__dirname, 'public'))); + // view engine setup + app.set('views', path.join(__dirname, 'views')); + app.set('view engine', 'pug'); + app.set('trust proxy', 1); -app.use('/', indexRouter); -app.use('/users', usersRouter); -app.use(/\/riddle(\/.*)?/, riddleRouter); -app.use('/bingo', bingoRouter); -app.use('/graphql', graphqlHTTP((request, response) => { - return { - schema: buildSchema(importSchema('./graphql/schema.graphql')), - rootValue: graphqlResolver(request, response), - context: {session: request.session}, - graphiql: true - }; -})); + app.use(logger('dev')); + app.use(express.json()); + app.use(express.urlencoded({ extended: false })); + app.use(cookieParser()); + app.use(session({ + store: new pgSession({ + pool: pgPool, + tableName: 'user_sessions' + }), + secret: settings.sessions.secret, + resave: false, + saveUninitialized: true, + cookie: { + maxAge: 30 * 24 * 60 * 60 * 1000 // maxAge 30 days + } + })); + app.use('/sass', compileSass({ + root: './public/stylesheets/sass', + sourceMap: true, + watchFiles: true, + logToConsole: true + })); + app.use(express.static(path.join(__dirname, 'public'))); + + app.use('/', indexRouter); + app.use('/users', usersRouter); + app.use(/\/riddle(\/.*)?/, riddleRouter); + app.use('/bingo', bingoRouter); + app.use('/graphql', graphqlHTTP((request, response) => { + return { + schema: buildSchema(importSchema('./graphql/schema.graphql')), + rootValue: graphqlResolver(request, response), + context: {session: request.session}, + graphiql: true + }; + })); // catch 404 and forward to error handler -app.use(function(req, res, next) { - next(createError(404)); -}); + app.use(function(req, res, next) { + next(createError(404)); + }); // error handler -app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; - // render the error page - res.status(err.status || 500); - res.render('error'); -}); + // render the error page + res.status(err.status || 500); + res.render('error'); + }); + return app; +} -module.exports = app; +module.exports = init; //app.listen(settings.port); diff --git a/bin/www b/bin/www index 7f43cbf..9c787e9 100644 --- a/bin/www +++ b/bin/www @@ -4,7 +4,7 @@ * Module dependencies. */ -const app = require('../app'); +const appInit = require('../app'); const debug = require('debug')('whooshy:server'); const http = require('http'); const yaml = require('js-yaml'); @@ -26,25 +26,31 @@ try { */ let port = normalizePort(process.env.PORT || settings.port || '3000'); -app.set('port', port); -/** - * Create HTTP server. - */ +appInit().then((app) => { + app.set('port', port); -let server = http.createServer(app); + /** + * Create HTTP server. + */ -/** - * Listen on provided port, on all network interfaces. - */ + let server = http.createServer(app); -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); + /** + * Listen on provided port, on all network interfaces. + */ -/** - * Normalize a port into a number, string, or false. - */ + server.listen(port); + server.on('error', (error) => onError(error, server)); + server.on('listening', () => onListening(server)); + + /** + * Normalize a port into a number, string, or false. + */ +}).catch((err) => { + console.error(err.message); + console.error(err.stack); +}); function normalizePort(val) { let port = parseInt(val, 10); @@ -66,7 +72,7 @@ function normalizePort(val) { * Event listener for HTTP server "error" event. */ -function onError(error) { +function onError(error, server) { if (error.syscall !== 'listen') { throw error; } @@ -94,7 +100,7 @@ function onError(error) { * Event listener for HTTP server "listening" event. */ -function onListening() { +function onListening(server) { let addr = server.address(); let bind = typeof addr === 'string' ? 'pipe ' + addr diff --git a/default-config.yaml b/default-config.yaml index 9edb835..3155f66 100644 --- a/default-config.yaml +++ b/default-config.yaml @@ -2,4 +2,11 @@ sessions: secret: averysecuresessionsecret maxAge: 1000000 -port: 3000 \ No newline at end of file +port: 3000 + +postgres: + host: localhost + port: 5432 + user: whooshy + password: whooshypassword + database: whooshy diff --git a/package-lock.json b/package-lock.json index 2621680..ff946f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -381,6 +381,11 @@ } } }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -572,6 +577,14 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "connect-pg-simple": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-5.0.0.tgz", + "integrity": "sha512-WZ7xkN+qe5bbDLgZ1L9GxnSbr155cJHmfNRzVR5hBvqio7Pg/vuH7Cf8lPUSFClQjtybYSejUqyO54sYt4cg+w==", + "requires": { + "pg": "^7.4.3" + } + }, "connect-sqlite3": { "version": "0.9.11", "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.11.tgz", @@ -2789,6 +2802,11 @@ "os-tmpdir": "^1.0.0" } }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -2850,6 +2868,62 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "pg": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-7.11.0.tgz", + "integrity": "sha512-YO4V7vCmEMGoF390LJaFaohWNKaA2ayoQOEZmiHVcAUF+YsRThpf/TaKCgSvsSE7cDm37Q/Cy3Gz41xiX/XjTw==", + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "0.1.3", + "pg-pool": "^2.0.4", + "pg-types": "~2.0.0", + "pgpass": "1.x", + "semver": "4.3.2" + }, + "dependencies": { + "semver": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", + "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" + } + } + }, + "pg-connection-string": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", + "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-hod2zYQxM8Gt482q+qONGTYcg/qVcV32VHVPtktbBJs0us3Dj7xibISw0BAAXVMCzt8A/jhfJvpZaxUlqtqs0g==" + }, + "pg-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.0.1.tgz", + "integrity": "sha512-b7y6QM1VF5nOeX9ukMQ0h8a9z89mojrBHXfJeSug4mhL0YpxNBm83ot2TROyoAmX/ZOX3UbwVO4EbH7i1ZZNiw==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", + "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", + "requires": { + "split": "^1.0.0" + } + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -2873,6 +2947,29 @@ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + }, + "postgres-date": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.4.tgz", + "integrity": "sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -3528,6 +3625,14 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "requires": { + "through": "2" + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -3696,6 +3801,11 @@ "inherits": "2" } }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", diff --git a/package.json b/package.json index 176f489..02b64d5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "start": "node ./bin/www" }, "dependencies": { - "connect-sqlite3": "^0.9.11", + "connect-pg-simple": "^5.0.0", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", @@ -22,6 +22,7 @@ "markdown-it-emoji": "^1.4.0", "morgan": "~1.9.1", "node-sass": "^4.12.0", + "pg": "^7.11.0", "pug": "2.0.0-beta11" } } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 59179f4..702a4d9 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -330,6 +330,17 @@ function submitOnEnter(event, func) { func(); } +/** + * Toggles the displayChat class on the content container to switch between chat-view and grid view + */ +function toggleChatView() { + let contentContainer = document.querySelector('#content-container'); + if (contentContainer.getAttribute('class') === 'displayChat') + contentContainer.setAttribute('class', ''); + else + contentContainer.setAttribute('class', 'displayChat') +} + window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { promiseRejectionEvent.promise.catch(err => console.log(err)); showError('Connection problems... Is the server down?'); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 07fb479..a8b1f55 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -21,12 +21,26 @@ textarea height: 80% #content-container grid-template-columns: 0 100% !important - grid-template-rows: 10% 80% 10% !important + grid-template-rows: 10% 40% 40% 10% !important #players-container, #chat-container display: none !important padding: 0 + .errorDiv + grid-column-start: 1 !important + grid-column-end: 4 !important + + #content-container.displayChat + grid-template-columns: 100% 0 !important + grid-template-rows: 0 25% 65% 10% !important + + #players-container, #chat-container + display: block !important + + #words-container + display: none !important + #username-form width: calc(100% - 2rem) !important left: 0 !important @@ -38,6 +52,23 @@ textarea width: calc(100% - 2rem) !important left: 0 !important + #button-container + grid-column-start: 2 !important + grid-column-end: 3 !important + + #chat-button-container + display: inline-block + grid-row-start: 4 + grid-row-end: 4 + grid-column-start: 1 + grid-column-end: 4 + overflow: hidden + margin: 0 0.5rem + + button + width: 100% + margin: 0.5rem 0 + @media(min-device-width: 641px) textarea height: 80% @@ -45,6 +76,8 @@ textarea #words-container width: 100% height: 100% + #chat-button-container + display: none .number-input width: 4rem @@ -179,7 +212,7 @@ textarea word-break: break-word #chat-content - height: calc(100% - 3.5rem) + height: calc(100% - 2.5rem) background-color: $primary overflow: auto @@ -199,7 +232,7 @@ textarea #chat-input width: 100% - margin: 1rem 0 0 0 + margin: 0 0 0 0 height: 2.5rem border-radius: 0 diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass index af8b73b..0b60140 100644 --- a/public/stylesheets/sass/mixins.sass +++ b/public/stylesheets/sass/mixins.sass @@ -4,5 +4,4 @@ background: lighten($primary, 10%) color: $primarySurface border: 2px solid $primarySurface - border-radius: $borderRadius transition-duration: 0.2s diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 3b56a96..b86a1e9 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -12,11 +12,11 @@ @media (min-device-width: 641px) html - font-size: 4vw + font-size: 2.5vw @media (min-device-width: 961px) html - font-size: 3vw + font-size: 2.2vw @media (min-device-width: 1025px) html @@ -63,8 +63,10 @@ a ::-webkit-scrollbar-thumb background: darken($secondary, 5) - border-radius: 10px + transition-duration: 0.2s + +::-webkit-scrollbar-thumb:hover + background: $secondary ::-webkit-scrollbar-track background: lighten($primary, 5) - border-radius: 10px diff --git a/sql/createSessionTable.sql b/sql/createSessionTable.sql new file mode 100644 index 0000000..50b1234 --- /dev/null +++ b/sql/createSessionTable.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS "user_sessions" ( + "sid" varchar NOT NULL COLLATE "default", + "sess" json NOT NULL, + "expire" timestamp(6) NOT NULL, + PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE +) +WITH (OIDS=FALSE); diff --git a/views/bingo/bingo-game.pug b/views/bingo/bingo-game.pug index 1f9974a..213edc0 100644 --- a/views/bingo/bingo-game.pug +++ b/views/bingo/bingo-game.pug @@ -23,4 +23,6 @@ block content 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! + button(id='bingo-button', onclick='submitBingo()', class='hidden') Bingo! + div(id='chat-button-container') + button(id='chat-toggle-button', onclick='toggleChatView()') toggle Chat From fe3fdecae10bcc9870ba962dab54b5460310586e Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 18:19:40 +0200 Subject: [PATCH 16/42] Bingo chat improvements - added more markdown plugins - fixed image size - fixed bug where join message appeared after site refresh --- package-lock.json | 217 ++++------------------- package.json | 3 + public/stylesheets/sass/bingo/style.sass | 10 ++ routes/bingo.js | 12 +- views/bingo/bingo-layout.pug | 1 + 5 files changed, 56 insertions(+), 187 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff946f8..774c924 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,6 +166,15 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, + "ascii2mathml": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ascii2mathml/-/ascii2mathml-0.6.2.tgz", + "integrity": "sha512-tkPONh2Y7ZpuGQw6AiRnExX/CSYf5C2T/rF+UMJq5n0Us7+QjL8VY5ZE16xo9wcXhdqPkl/F7lzEzU9HX0YKng==", + "optional": true, + "requires": { + "minimist": "^1.2.0" + } + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -479,11 +488,6 @@ "upath": "^1.1.1" } }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -585,14 +589,6 @@ "pg": "^7.4.3" } }, - "connect-sqlite3": { - "version": "0.9.11", - "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.11.tgz", - "integrity": "sha1-TlQVXcLq3ypZmDhbLzXoPdkkCy0=", - "requires": { - "sqlite3": "^4.0.0" - } - }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -701,11 +697,6 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -768,11 +759,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -1167,14 +1153,6 @@ "universalify": "^0.1.0" } }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "requires": { - "minipass": "^2.2.1" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1876,14 +1854,6 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "requires": { - "minimatch": "^3.0.4" - } - }, "in-publish": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", @@ -1911,11 +1881,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, "inline-source-map-comment": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/inline-source-map-comment/-/inline-source-map-comment-1.0.5.tgz", @@ -2303,6 +2268,29 @@ "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" }, + "markdown-it-linkify-images": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-1.1.0.tgz", + "integrity": "sha1-xVedk4bXcgxUbPWD/glq3FmAyFk=" + }, + "markdown-it-mark": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-2.0.0.tgz", + "integrity": "sha1-RqGqlHEFrtgYiXjgoBYXnkBPQsc=" + }, + "markdown-it-math": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-math/-/markdown-it-math-4.1.1.tgz", + "integrity": "sha512-LQ0hREgMgN4tNcy2PGyw1XypjmKJjc+ZzATMuDIVD/Bagr5SGL198uHleVdiFDrNdXpqVmL4N1KD1GYyftMakQ==", + "requires": { + "ascii2mathml": "^0.6.2" + } + }, + "markdown-it-smartarrows": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-smartarrows/-/markdown-it-smartarrows-1.0.1.tgz", + "integrity": "sha1-tXDpwP+YEuDbas4Zr6W6ErZLuac=" + }, "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -2398,30 +2386,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "requires": { - "minipass": "^2.2.1" - } - }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", @@ -2503,31 +2467,6 @@ } } }, - "needle": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz", - "integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==", - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -2559,53 +2498,6 @@ } } }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, "node-sass": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", @@ -2678,20 +2570,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, - "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -3147,17 +3025,6 @@ "unpipe": "1.0.0" } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -3389,11 +3256,6 @@ } } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -3646,16 +3508,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "sqlite3": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.8.tgz", - "integrity": "sha512-kgwHu4j10KhpCHtx//dejd/tVQot7jc3sw+Sn0vMuKOw0X00Ckyg9VceKgzPyGmmz+zEoYue9tOLriWTvYy0ww==", - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.11.0", - "request": "^2.87.0" - } - }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -3746,11 +3598,6 @@ "get-stdin": "^4.0.1" } }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, "sum-up": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", diff --git a/package.json b/package.json index 02b64d5..6aec647 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "js-yaml": "latest", "markdown-it": "^8.4.2", "markdown-it-emoji": "^1.4.0", + "markdown-it-mark": "^2.0.0", + "markdown-it-math": "^4.1.1", + "markdown-it-smartarrows": "^1.0.1", "morgan": "~1.9.1", "node-sass": "^4.12.0", "pg": "^7.11.0", diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index a8b1f55..88d0c3e 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -215,6 +215,7 @@ textarea height: calc(100% - 2.5rem) background-color: $primary overflow: auto + font-size: 0.8em .chatMessage display: list-item @@ -230,6 +231,15 @@ textarea color: $inactive font-style: italic + .chatMessageContent + img + width: 100% + height: auto + transition-duration: 0.5s + border-radius: 0.5em + img:hover + border-radius: 0 + #chat-input width: 100% margin: 0 0 0 0 diff --git a/routes/bingo.js b/routes/bingo.js index b14b677..06bb098 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -3,7 +3,14 @@ const express = require('express'), cproc = require('child_process'), fsx = require('fs-extra'), mdEmoji = require('markdown-it-emoji'), - md = require('markdown-it')().use(mdEmoji); + mdMark = require('markdown-it-mark'), + mdSmartarrows = require('markdown-it-smartarrows'), + mdMath = require('markdown-it-math'), + md = require('markdown-it')() + .use(mdEmoji) + .use(mdMark) + .use(mdSmartarrows) + .use(mdMath); const rWordOnly = /^\w+$/; @@ -303,7 +310,8 @@ router.get('/', (req, res) => { if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { bingoUser.game = gameId; let bingoSession = bingoSessions[gameId]; - bingoSession.addUser(bingoUser); + if (!bingoSession.users[bingoUser.id]) + bingoSession.addUser(bingoUser); if (!bingoUser.grids[gameId]) { bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words); diff --git a/views/bingo/bingo-layout.pug b/views/bingo/bingo-layout.pug index 9549b96..e0c91ba 100644 --- a/views/bingo/bingo-layout.pug +++ b/views/bingo/bingo-layout.pug @@ -4,5 +4,6 @@ html title Bingo by Trivernis script(type='text/javascript', src='/javascripts/bingo-web.js') link(rel='stylesheet', href='/sass/bingo/style.sass') + base(target='_blank') body block content From 7496d1d112bc04d070868afb59c2fe9ce13c50f5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 12 May 2019 16:41:13 +0000 Subject: [PATCH 17/42] Update dependency pug to v2.0.3 --- package-lock.json | 202 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 114 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index 594a9b5..bf243ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,16 @@ "kind-of": "^3.0.2", "longest": "^1.0.1", "repeat-string": "^1.5.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } } }, "amdefine": { @@ -291,11 +301,6 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -496,12 +501,18 @@ } }, "clean-css": { - "version": "3.4.28", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", - "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", "requires": { - "commander": "2.8.x", - "source-map": "0.4.x" + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } } }, "cliui": { @@ -549,14 +560,6 @@ "delayed-stream": "~1.0.0" } }, - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -709,11 +712,6 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -1011,11 +1009,6 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -1721,11 +1714,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, "graphql": { "version": "14.3.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.3.0.tgz", @@ -1925,6 +1913,16 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } } }, "is-arrayish": { @@ -1951,6 +1949,16 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "requires": { "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } } }, "is-descriptor": { @@ -2026,6 +2034,16 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "requires": { "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } } }, "is-plain-object": { @@ -2157,12 +2175,9 @@ } }, "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, "lazy-cache": { "version": "1.0.4", @@ -2285,13 +2300,6 @@ "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } } }, "mime": { @@ -2397,13 +2405,6 @@ "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } } }, "negotiator": { @@ -2552,6 +2553,14 @@ "requires": { "is-descriptor": "^0.1.0" } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } } } }, @@ -2736,18 +2745,18 @@ "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" }, "pug": { - "version": "2.0.0-beta11", - "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.0-beta11.tgz", - "integrity": "sha1-Favmr1AEx+LPRhPksnRlyVRrXwE=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz", + "integrity": "sha1-ccuoJTfJWl6rftBGluQiH1Oqh44=", "requires": { - "pug-code-gen": "^1.1.1", - "pug-filters": "^2.1.1", - "pug-lexer": "^3.0.0", - "pug-linker": "^2.0.2", - "pug-load": "^2.0.5", - "pug-parser": "^2.0.2", - "pug-runtime": "^2.0.3", - "pug-strip-comments": "^1.0.2" + "pug-code-gen": "^2.0.1", + "pug-filters": "^3.1.0", + "pug-lexer": "^4.0.0", + "pug-linker": "^3.0.5", + "pug-load": "^2.0.11", + "pug-parser": "^5.0.0", + "pug-runtime": "^2.0.4", + "pug-strip-comments": "^1.0.3" } }, "pug-attrs": { @@ -2761,16 +2770,16 @@ } }, "pug-code-gen": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-1.1.1.tgz", - "integrity": "sha1-HPcnRO8qA56uajNAyqoRBYcSWOg=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.1.tgz", + "integrity": "sha1-CVHsgyJddNjPxHan+Zolm199BQw=", "requires": { "constantinople": "^3.0.1", "doctypes": "^1.1.0", "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.2", + "pug-attrs": "^2.0.3", "pug-error": "^1.3.2", - "pug-runtime": "^2.0.3", + "pug-runtime": "^2.0.4", "void-elements": "^2.0.1", "with": "^5.0.0" } @@ -2781,23 +2790,23 @@ "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" }, "pug-filters": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-2.1.5.tgz", - "integrity": "sha512-xkw71KtrC4sxleKiq+cUlQzsiLn8pM5+vCgkChW2E6oNOzaqTSIBKIQ5cl4oheuDzvJYCTSYzRaVinMUrV4YLQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.0.tgz", + "integrity": "sha1-JxZVVbwEwjbkqisDZiRt+gIbYm4=", "requires": { - "clean-css": "^3.3.0", + "clean-css": "^4.1.11", "constantinople": "^3.0.1", "jstransformer": "1.0.0", "pug-error": "^1.3.2", - "pug-walk": "^1.1.5", + "pug-walk": "^1.1.7", "resolve": "^1.1.6", "uglify-js": "^2.6.1" } }, "pug-lexer": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-3.1.0.tgz", - "integrity": "sha1-/QhzdtSmdbT1n4/vQiiDQ06VgaI=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz", + "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", "requires": { "character-parser": "^2.1.1", "is-expression": "^3.0.0", @@ -2805,12 +2814,12 @@ } }, "pug-linker": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-2.0.3.tgz", - "integrity": "sha1-szH/olc33eacEntWwQ/xf652bco=", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.5.tgz", + "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=", "requires": { "pug-error": "^1.3.2", - "pug-walk": "^1.1.2" + "pug-walk": "^1.1.7" } }, "pug-load": { @@ -2823,9 +2832,9 @@ } }, "pug-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-2.0.2.tgz", - "integrity": "sha1-U6aAz9BQOdywwn0CkJS8SnkmibA=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz", + "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", "requires": { "pug-error": "^1.3.2", "token-stream": "0.0.1" @@ -3273,11 +3282,6 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -3287,6 +3291,16 @@ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "requires": { "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } } }, "source-map": { @@ -3506,6 +3520,16 @@ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "requires": { "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } } }, "to-regex": { diff --git a/package.json b/package.json index 5045c6d..144b352 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,6 @@ "js-yaml": "latest", "morgan": "1.9.1", "node-sass": "4.12.0", - "pug": "2.0.0-beta11" + "pug": "2.0.3" } } From 07dec6f94324dda11399af4a3c1559207822e536 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 12 May 2019 16:41:38 +0000 Subject: [PATCH 18/42] Update dependency debug to v4 --- package-lock.json | 89 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 594a9b5..2a38ff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -343,6 +343,16 @@ "qs": "6.5.2", "raw-body": "2.3.3", "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } } }, "brace-expansion": { @@ -658,11 +668,18 @@ } }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } } }, "decamelize": { @@ -808,6 +825,14 @@ "to-regex": "^3.0.1" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", @@ -861,6 +886,16 @@ "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } } }, "express-compile-sass": { @@ -924,6 +959,14 @@ "uid-safe": "~2.1.5" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1067,6 +1110,16 @@ "parseurl": "~1.3.2", "statuses": "~1.4.0", "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } } }, "find-up": { @@ -2369,6 +2422,16 @@ "depd": "~1.1.2", "on-finished": "~2.3.0", "on-headers": "~1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } } }, "ms": { @@ -3143,6 +3206,16 @@ "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } } }, "serve-static": { @@ -3207,6 +3280,14 @@ "use": "^3.1.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", diff --git a/package.json b/package.json index 5045c6d..e83db67 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "cookie-parser": "1.4.4", - "debug": "2.6.9", + "debug": "4.1.1", "express": "4.16.4", "express-compile-sass": "latest", "express-graphql": "0.8.0", From 28aaff7962a81e97d5724369e2f56068062de533 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 12 May 2019 16:48:24 +0000 Subject: [PATCH 19/42] Update dependency http-errors to v1.7.2 --- package-lock.json | 58 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72d32b4..ffc247d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -357,6 +357,17 @@ "requires": { "ms": "2.0.0" } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } } } }, @@ -1859,14 +1870,27 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" }, "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "requires": { "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } } }, "http-signature": { @@ -2950,6 +2974,19 @@ "http-errors": "1.6.3", "iconv-lite": "0.4.23", "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + } } }, "read-pkg": { @@ -3224,6 +3261,17 @@ "requires": { "ms": "2.0.0" } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } } } }, diff --git a/package.json b/package.json index 7d5229c..e716cac 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "fs-extra": "7.0.1", "graphql": "14.3.0", "graphql-import": "0.7.1", - "http-errors": "1.6.3", + "http-errors": "1.7.2", "js-yaml": "latest", "morgan": "1.9.1", "node-sass": "4.12.0", From efede7d9256afb297c4dbbcd46659ed00c290aa1 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 18:58:18 +0200 Subject: [PATCH 20/42] Auto stash before merge of "develop" and "GitHub-whooshy/develop" --- package-lock.json | 22 ---------------------- package.json | 1 - routes/bingo.js | 6 ++---- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 997d613..b62381c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -176,15 +176,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "ascii2mathml": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/ascii2mathml/-/ascii2mathml-0.6.2.tgz", - "integrity": "sha512-tkPONh2Y7ZpuGQw6AiRnExX/CSYf5C2T/rF+UMJq5n0Us7+QjL8VY5ZE16xo9wcXhdqPkl/F7lzEzU9HX0YKng==", - "optional": true, - "requires": { - "minimist": "^1.2.0" - } - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -2379,24 +2370,11 @@ "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", "integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=" }, - "markdown-it-linkify-images": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-1.1.0.tgz", - "integrity": "sha1-xVedk4bXcgxUbPWD/glq3FmAyFk=" - }, "markdown-it-mark": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-2.0.0.tgz", "integrity": "sha1-RqGqlHEFrtgYiXjgoBYXnkBPQsc=" }, - "markdown-it-math": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/markdown-it-math/-/markdown-it-math-4.1.1.tgz", - "integrity": "sha512-LQ0hREgMgN4tNcy2PGyw1XypjmKJjc+ZzATMuDIVD/Bagr5SGL198uHleVdiFDrNdXpqVmL4N1KD1GYyftMakQ==", - "requires": { - "ascii2mathml": "^0.6.2" - } - }, "markdown-it-smartarrows": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/markdown-it-smartarrows/-/markdown-it-smartarrows-1.0.1.tgz", diff --git a/package.json b/package.json index d93ee9d..27413cd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "markdown-it": "^8.4.2", "markdown-it-emoji": "^1.4.0", "markdown-it-mark": "^2.0.0", - "markdown-it-math": "^4.1.1", "markdown-it-smartarrows": "^1.0.1", "morgan": "1.9.1", "node-sass": "4.12.0", diff --git a/routes/bingo.js b/routes/bingo.js index 06bb098..8aa7808 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -5,12 +5,10 @@ const express = require('express'), mdEmoji = require('markdown-it-emoji'), mdMark = require('markdown-it-mark'), mdSmartarrows = require('markdown-it-smartarrows'), - mdMath = require('markdown-it-math'), md = require('markdown-it')() .use(mdEmoji) .use(mdMark) - .use(mdSmartarrows) - .use(mdMath); + .use(mdSmartarrows); const rWordOnly = /^\w+$/; @@ -67,7 +65,7 @@ class BingoSession { this.followup = followup.id; bingoSessions[followup.id] = followup; followup.chatMessages = this.chatMessages; - followup.chatMessages.push(new BingoChatMessage('**Rematch**', "INFO")); + followup.chatMessages.push(new BingoChatMessage('==**Rematch**==', "INFO")); return followup; } From d89137924a88e3e49d915468686b0a2cf81a977f Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 19:00:29 +0200 Subject: [PATCH 21/42] Auto stash before merge of "develop" and "GitHub-whooshy/develop" --- package-lock.json | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index b62381c..7011e05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1223,8 +1223,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -1242,13 +1241,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1261,18 +1258,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -1375,8 +1369,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -1386,7 +1379,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1399,20 +1391,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1429,7 +1418,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -1502,8 +1490,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -1513,7 +1500,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -1589,8 +1575,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -1620,7 +1605,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -1638,7 +1622,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1677,13 +1660,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, From 5ca1831e5efa5a86fd9f2994f32816d9125a4a43 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 19:19:49 +0200 Subject: [PATCH 22/42] Create LICENSE --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 2fb44df24dbb6ddde4655b7ca61a385c0cfc8d50 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 19:20:45 +0200 Subject: [PATCH 23/42] Rename LICENSE to LICENSE.md --- LICENSE => LICENSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.md (100%) diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md From 52bd2378fb65172c433ad0fcc2c269aba265ccd7 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 20:25:58 +0200 Subject: [PATCH 24/42] Fixed issues - fixed bingo button not shown on refresh - fixed code style - added eslint --- CHANGELOG.md | 5 +- README.md | 2 +- app.js | 4 +- bin/www | 15 +- package-lock.json | 724 +++++++++++++++++++++++ package.json | 66 +++ public/javascripts/bingo-web.js | 91 ++- public/javascripts/common.js | 45 +- public/javascripts/riddle-web.js | 23 +- public/stylesheets/sass/bingo/style.sass | 6 +- public/stylesheets/sass/style.sass | 4 + routes/bingo.js | 124 ++-- routes/index.js | 2 +- routes/riddle.js | 23 +- routes/users.js | 4 +- 15 files changed, 1013 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 307231e..ea6f65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Chat to the bingo game (renderd with markdown-it) - Postgres session storage - sql-file directory `sql` - +- LICENSE.md (GPL v3) +- eslint to dev dependencys ## Changed @@ -28,3 +29,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - mobile layout +- code style issues +- Bingo button not shown on refresh diff --git a/README.md b/README.md index 26dab37..8b99c27 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# whooshy +# whooshy [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) This repository is the node.js webserver running on `(beta.)trivernis.net`. diff --git a/app.js b/app.js index a6b7c73..858aac3 100644 --- a/app.js +++ b/app.js @@ -29,7 +29,7 @@ async function init() { return { time: Date.now(), bingo: bingoRouter.graphqlResolver(request, response) - } + }; }; // database setup let pgPool = new pg.Pool({ @@ -91,7 +91,7 @@ async function init() { }); // error handler - app.use(function(err, req, res, next) { + app.use(function(err, req, res) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; diff --git a/bin/www b/bin/www index 9c787e9..2a09075 100644 --- a/bin/www +++ b/bin/www @@ -41,7 +41,7 @@ appInit().then((app) => { */ server.listen(port); - server.on('error', (error) => onError(error, server)); + server.on('error', (error) => onError(error)); server.on('listening', () => onListening(server)); /** @@ -55,16 +55,12 @@ appInit().then((app) => { function normalizePort(val) { let port = parseInt(val, 10); - if (isNaN(port)) { + if (isNaN(port)) // named pipe return val; - } - - if (port >= 0) { + if (port >= 0) // port number return port; - } - return false; } @@ -72,10 +68,9 @@ function normalizePort(val) { * Event listener for HTTP server "error" event. */ -function onError(error, server) { - if (error.syscall !== 'listen') { +function onError(error) { + if (error.syscall !== 'listen') throw error; - } let bind = typeof port === 'string' ? 'Pipe ' + port diff --git a/package-lock.json b/package-lock.json index 7011e05..9643799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,26 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, "@types/babel-types": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", @@ -51,6 +71,12 @@ } } }, + "acorn-jsx": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -87,6 +113,12 @@ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -194,6 +226,12 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", @@ -433,6 +471,12 @@ "unset-value": "^1.0.0" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -486,6 +530,12 @@ "is-regex": "^1.0.3" } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "chokidar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", @@ -541,6 +591,21 @@ } } }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -664,6 +729,16 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-fetch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz", + "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=", + "dev": true, + "requires": { + "node-fetch": "2.1.2", + "whatwg-fetch": "2.0.4" + } + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -719,6 +794,12 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -776,6 +857,15 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "doctypes": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", @@ -795,6 +885,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -823,11 +919,166 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "eslint-plugin-graphql": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-graphql/-/eslint-plugin-graphql-3.0.3.tgz", + "integrity": "sha512-hHwLyxSkC5rkakJ/SNTWwOswPdVhvfyMCnEOloevrLQIOHUNVIQBg1ljCaRe9C40HdzgcGUFUdG5BHLCKm8tuw==", + "dev": true, + "requires": { + "graphql-config": "^2.0.1", + "lodash": "^4.11.1" + } + }, + "eslint-plugin-promise": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz", + "integrity": "sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==", + "dev": true + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -1025,6 +1276,28 @@ } } }, + "external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -1099,6 +1372,30 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1153,6 +1450,23 @@ "pinkie-promise": "^2.0.0" } }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz", + "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -1684,6 +1998,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -1762,6 +2082,12 @@ } } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "globule": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", @@ -1785,6 +2111,19 @@ "iterall": "^1.2.2" } }, + "graphql-config": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-2.2.1.tgz", + "integrity": "sha512-U8+1IAhw9m6WkZRRcyj8ZarK96R6lQBQ0an4lp76Ps9FyhOXENC5YQOxOFGm5CxPrX2rD0g3Je4zG5xdNJjwzQ==", + "dev": true, + "requires": { + "graphql-import": "^0.7.1", + "graphql-request": "^1.5.0", + "js-yaml": "^3.10.0", + "lodash": "^4.17.4", + "minimatch": "^3.0.4" + } + }, "graphql-import": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", @@ -1794,6 +2133,15 @@ "resolve-from": "^4.0.0" } }, + "graphql-request": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz", + "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==", + "dev": true, + "requires": { + "cross-fetch": "2.2.2" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1910,6 +2258,28 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", + "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "in-publish": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", @@ -1973,6 +2343,79 @@ } } }, + "inquirer": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", + "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.11", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + } + } + }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -2193,6 +2636,12 @@ "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -2217,6 +2666,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2268,6 +2723,16 @@ "invert-kv": "^1.0.0" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "linkify-it": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", @@ -2436,6 +2901,12 @@ "mime-db": "1.40.0" } }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2510,6 +2981,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, "nan": { "version": "2.13.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", @@ -2533,11 +3010,29 @@ "to-regex": "^3.0.1" } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=", + "dev": true + }, "node-gyp": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", @@ -2727,6 +3222,37 @@ "wrappy": "1" } }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -2759,6 +3285,15 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -2795,6 +3330,18 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -2922,11 +3469,23 @@ "xtend": "^4.0.0" } }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -3178,6 +3737,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -3256,6 +3821,16 @@ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -3277,6 +3852,24 @@ "glob": "^7.1.3" } }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", + "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3440,11 +4033,45 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -3719,6 +4346,12 @@ "get-stdin": "^4.0.1" } }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "sum-up": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", @@ -3759,6 +4392,52 @@ "has-flag": "^3.0.0" } }, + "table": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/table/-/table-5.3.3.tgz", + "integrity": "sha512-3wUNCgdWX6PNpOe3amTTPWPuF6VGvgzjKCaO1snFj0z7Y3mUPWf5+zDtxUVGispJkDECPmR29wbzh6bVMOHbcw==", + "dev": true, + "requires": { + "ajv": "^6.9.1", + "lodash": "^4.17.11", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "tar": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", @@ -3769,11 +4448,26 @@ "inherits": "2" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", @@ -3856,6 +4550,12 @@ "glob": "^7.1.2" } }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3869,6 +4569,15 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4059,6 +4768,12 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -4113,6 +4828,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index 27413cd..f5fc6d9 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,71 @@ "node-sass": "4.12.0", "pg": "^7.11.0", "pug": "2.0.3" + }, + "devDependencies": { + "eslint": "^5.16.0", + "eslint-plugin-graphql": "^3.0.3", + "eslint-plugin-promise": "^4.1.1" + }, + "eslintConfig": { + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "env": { + "node": true, + "browser": true, + "jquery": true, + "es6": true + }, + "extends": [ + "eslint:recommended", + "plugin:promise/recommended" + ], + "rules": { + "semi": "error", + "semi-style": [ + "error", + "last" + ], + "no-await-in-loop": "warn", + "curly": [ + "warn", + "multi", + "consistent" + ], + "block-spacing": [ + "warn", + "always" + ], + "array-bracket-newline": [ + "warn", + "consistent" + ], + "camelcase": [ + "error", + { + "properties": "always" + } + ], + "comma-spacing": [ + "error", + { + "after": true + } + ], + "brace-style": [ + "error", + "1tbs" + ], + "no-console": "off", + "promise/no-promise-in-callback": "off", + "promise/always-return": "off", + "promise/catch-or-return": "off" + }, + "plugins": [ + "eslint-plugin-graphql", + "eslint-plugin-promise" + ] } } diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 702a4d9..0b953c6 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars, no-undef */ /** * Returns the value of the url-param 'game' * @returns {string} @@ -17,7 +18,7 @@ function getGameParam() { async function submitBingoWords() { let textContent = document.querySelector('#bingo-textarea').value; let words = textContent.replace(/[<>]/g, '').split('\n').filter((el) => { - return (!!el && el.length > 0) // remove empty strings and non-types from word array + 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!'); @@ -43,7 +44,7 @@ async function submitBingoWords() { insertParam('game', gameid); } else { showError(`Failed to create game. HTTP Error: ${response.status}`); - console.error(response) + console.error(response); } } } @@ -60,7 +61,7 @@ async function createFollowup() { id } } - }`,null,`/graphql?game=${getGameParam()}`); + }`, null, `/graphql?game=${getGameParam()}`); if (response.status === 200 && response.data.bingo.createFollowupGame) { let gameid = response.data.bingo.createFollowupGame.id; insertParam('game', gameid); @@ -88,7 +89,7 @@ async function submitUsername() { } }`, { username: username - },`/graphql?game=${getGameParam()}`); + }, `/graphql?game=${getGameParam()}`); if (response.status === 200) { unameInput.value = ''; unameInput.placeholder = response.data.username; @@ -122,22 +123,59 @@ async function submitWord(word) { } }`, { word: word - },`/graphql?game=${getGameParam()}`); + }, `/graphql?game=${getGameParam()}`); 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) { + 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) { + + if (response.data.bingo.toggleWord.bingo) document.querySelector('#bingo-button').setAttribute('class', ''); - } else { + else document.querySelector('#bingo-button').setAttribute('class', 'hidden'); + + } else { + showError(`Failed to submit word. HTTP Error: ${response.status}`); + console.error(response); + } +} + +/** + * Refreshes the bingo grid. Shows the bingo button if a bingo is possible + * @returns {Promise} + */ +async function refreshBingoGrid() { + let response = await postGraphqlQuery(` + query { + bingo { + activeGrid { + bingo + fieldGrid { + word + base64Word + submitted + } } + } + }`, {}, `/graphql?game=${getGameParam()}`); + + if (response.status === 200 && response.data.bingo.activeGrid) { + let fieldGrid = response.data.bingo.activeGrid.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.activeGrid.bingo) + document.querySelector('#bingo-button').setAttribute('class', ''); + else + document.querySelector('#bingo-button').setAttribute('class', 'hidden'); + } else { showError(`Failed to submit word. HTTP Error: ${response.status}`); console.error(response); @@ -162,12 +200,12 @@ async function submitBingo() { } } } - }`,null,`/graphql?game=${getGameParam()}`); + }`, 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) + clearInterval(refrInterval); } } else { showError(`Failed to submit Bingo. HTTP Error: ${response.status}`); @@ -181,6 +219,7 @@ async function submitBingo() { * @returns {Promise} */ async function refresh() { + await refreshBingoGrid(); let response = await postGraphqlQuery(` query { bingo { @@ -205,7 +244,7 @@ async function refresh() { if (bingoSession.bingos.length > 0) { displayWinner(bingoSession.players.find(x => x.id === bingoSession.bingos[0]).username); - clearInterval(refrInterval) + clearInterval(refrInterval); } else { for (let player of bingoSession.players) { let foundPlayerDiv = document.querySelector(`.player-container[b-pid='${player.id}'`); @@ -217,16 +256,16 @@ async function refresh() { document.querySelector('#players-container').appendChild(playerDiv); } else { let playerNameSpan = foundPlayerDiv.querySelector('.player-name-span'); - if (playerNameSpan.innerText !== player.username) { + if (playerNameSpan.innerText !== player.username) playerNameSpan.innerText = player.username; - } + } } } - for (let chatMessage of bingoSession.getMessages) { + for (let chatMessage of bingoSession.getMessages) if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`)) addChatMessage(chatMessage); - } + } else { if (response.status === 400) clearInterval(refrInterval); @@ -288,7 +327,7 @@ async function sendChatMessage() { type } } - }`,{message: message}, `/graphql?game=${getGameParam()}`); + }`, {message: message}, `/graphql?game=${getGameParam()}`); if (response.status === 200) { addChatMessage(response.data.bingo.sendChatMessage); messageInput.value = ''; @@ -307,14 +346,14 @@ function addChatMessage(messageObject) { let msgSpan = document.createElement('span'); msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('msg-id', messageObject.id); - if (messageObject.type === "USER") { + if (messageObject.type === "USER") msgSpan.innerHTML = ` ${messageObject.username}: ${messageObject.htmlContent}`; - } else { + else msgSpan.innerHTML = ` ${messageObject.htmlContent}`; - } + let chatContent = document.querySelector('#chat-content'); chatContent.appendChild(msgSpan); chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom @@ -338,7 +377,7 @@ function toggleChatView() { if (contentContainer.getAttribute('class') === 'displayChat') contentContainer.setAttribute('class', ''); else - contentContainer.setAttribute('class', 'displayChat') + contentContainer.setAttribute('class', 'displayChat'); } window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { @@ -349,9 +388,9 @@ window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { window.onload = () => { if (document.querySelector('#chat-container')) refresh(); - if (window && !document.querySelector('#bingoform')) { - refrInterval = setInterval(refresh, 1000); // global variable to clear - } + if (window && !document.querySelector('#bingoform')) + refrInterval = setInterval(refresh, 1000); // eslint-disable-line no-undef + let gridSizeElem = document.querySelector('#bingo-grid-size'); document.querySelector('#bingo-grid-y').innerText = gridSizeElem.value; gridSizeElem.oninput = () => { diff --git a/public/javascripts/common.js b/public/javascripts/common.js index 15840a7..b38cacd 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -1,16 +1,24 @@ +/* eslint-disable no-unused-vars, no-undef */ + +/** + * HTTP POST to an url with a post body + * @param url {String} - the url to post to + * @param postBody {JSON|Object} - the json-object to post + * @returns {Promise} + */ function postData(url, postBody) { let request = new XMLHttpRequest(); - return new Promise((res, rej) => { + return new Promise((resolve, reject) => { request.onload = () => { - res({ + resolve({ status: request.status, data: request.responseText }); }; request.onerror = () => { - rej(request.error); + reject(request.error); }; request.open('POST', url, true); @@ -19,10 +27,22 @@ function postData(url, postBody) { }); } +/** + * HTTP POST to the current url + * @param postBody {JSON|Object} - the json-object to post + * @returns {Promise} + */ async function postLocData(postBody) { return await postData('#', postBody); } +/** + * HTTP POST to a graphql url endpoint (default '/graphql') + * @param query {String} - the graphql query to post + * @param [variables] {JSON} - optional variables used in the graphql query + * @param [url] {String} - optional alternative graphql endpoint + * @returns {Promise<{data: *, status: *}|{data: *, requestBody: {variables: *, query: *}, errors: *, status: *}>} + */ async function postGraphqlQuery(query, variables, url) { let body = { query: query, @@ -31,29 +51,33 @@ async function postGraphqlQuery(query, variables, url) { let response = await postData(url || '/graphql', body); let resData = JSON.parse(response.data); - if (response.status === 200) { + if (response.status === 200) return { status: response.status, data: resData.data, }; - } else { + else return { status: response.status, data: resData.data, errors: resData.errors, requestBody: body }; - } + } +/** + * Inserts an url parameter + * @param key {String} - the key of the url parameter + * @param value {String} - the value of the url parameter + */ function insertParam(key, value) { key = encodeURI(key); value = encodeURI(value); - let kvp = document.location.search.substr(1).split('&'); - let i = kvp.length; let x; + while (i--) { x = kvp[i].split('='); @@ -63,10 +87,7 @@ function insertParam(key, value) { break; } } - - if (i < 0) { + if (i < 0) kvp[kvp.length] = [key, value].join('='); - } - document.location.search = kvp.join('&'); } diff --git a/public/javascripts/riddle-web.js b/public/javascripts/riddle-web.js index 3329b11..8921410 100644 --- a/public/javascripts/riddle-web.js +++ b/public/javascripts/riddle-web.js @@ -1,3 +1,10 @@ +/* eslint-disable no-unused-vars, no-undef */ + +/** + * start the download of a subreddit + * @param subredditName {String} - the name of the subreddit to download + * @returns {Promise} + */ async function startSubredditDownload(subredditName) { let data = await postLocData({ subreddit: subredditName @@ -5,6 +12,11 @@ async function startSubredditDownload(subredditName) { return JSON.parse(data.data); } +/** + * Get the status of a download + * @param downloadId {String} - the id of the download to get the status for + * @returns {Promise} + */ async function getDownloadStatus(downloadId) { let data = await postLocData({ id: downloadId @@ -12,6 +24,11 @@ async function getDownloadStatus(downloadId) { return JSON.parse(data.data); } +/** + * refreshes information about a specific download + * @param downloadId {String} - the id of the download + * @returns {Promise} + */ async function refreshDownloadInfo(downloadId) { let response = await getDownloadStatus(downloadId); @@ -20,7 +37,7 @@ async function refreshDownloadInfo(downloadId) { let subredditName = dlDiv.getAttribute('subreddit-name'); if (response.status === 'pending') { - setTimeout(() => refreshDownloadInfo(downloadId), 1000) + setTimeout(() => refreshDownloadInfo(downloadId), 1000); } else { let dlLink = document.createElement('a'); dlLink.setAttribute('href', response.file); @@ -34,6 +51,10 @@ async function refreshDownloadInfo(downloadId) { } } +/** + * Submit a subreddit to download (called by button) + * @returns {Promise} + */ async function submitDownload() { let subredditName = document.querySelector('#subreddit-input').value; let response = await startSubredditDownload(subredditName); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 88d0c3e..5a95460 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -69,6 +69,9 @@ textarea width: 100% margin: 0.5rem 0 + #chat-container .chatMessage + font-size: 1.2em + @media(min-device-width: 641px) textarea height: 80% @@ -177,7 +180,6 @@ textarea grid-row-start: 1 grid-row-end: 1 display: grid - margin: 1rem font-size: inherit button @@ -206,7 +208,7 @@ textarea grid-column-end: 1 grid-row-start: 3 grid-row-end: 4 - height: 100% + height: calc(100% - 3px) border: 1px solid $inactive margin: 0 0.5rem word-break: break-word diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index b86a1e9..16f0db6 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -57,6 +57,10 @@ textarea a color: $secondary +mark + background-color: $secondary + color: $primarySurface + ::-webkit-scrollbar width: 12px height: 12px diff --git a/routes/bingo.js b/routes/bingo.js index 8aa7808..fcf0bdf 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1,7 +1,5 @@ const express = require('express'), router = express.Router(), - cproc = require('child_process'), - fsx = require('fs-extra'), mdEmoji = require('markdown-it-emoji'), mdMark = require('markdown-it-mark'), mdSmartarrows = require('markdown-it-smartarrows'), @@ -10,8 +8,6 @@ const express = require('express'), .use(mdMark) .use(mdSmartarrows); -const rWordOnly = /^\w+$/; - let bingoSessions = {}; class BingoSession { @@ -38,9 +34,9 @@ class BingoSession { addUser(user) { let id = user.id; this.users[id] = user; - if (user.username !== 'anonymous') { + if (user.username !== 'anonymous') this.chatMessages.push(new BingoChatMessage(`**${user.username}** joined.`, "INFO")); - } + } /** @@ -76,13 +72,13 @@ class BingoSession { */ getMessages(args) { let input = args.input || null; - if (input && input.id) { + if (input && input.id) return this.chatMessages.find(x => (x && x.id === input.id)); - } else if (input && input.last) { + else if (input && input.last) return this.chatMessages.slice(-input.last); - } else { + else return this.chatMessages.slice(-10); - } + } /** @@ -190,7 +186,7 @@ function inflateArray(array, minSize) { let iterations = Math.ceil(minSize/array.length)-1; for (let i = 0; i < iterations; i++) resultArray = [...resultArray, ...resultArray]; - return resultArray + return resultArray; } /** @@ -212,9 +208,9 @@ function generateWordGrid(dimensions, words) { let grid = []; for (let x = 0; x < dimensions[1]; x++) { grid[x] = []; - for (let y = 0; y < dimensions[0]; y++) { + for (let y = 0; y < dimensions[0]; y++) grid[x][y] = shuffledWords[(x * dimensions[0]) + y]; - } + } return (new BingoGrid(grid)); } @@ -235,55 +231,79 @@ function toggleHeared(base64Word, bingoGrid) { } /** - * Checks if a bingo exists in the bingo grid. - * @param bingoGrid {BingoGrid} - * @returns {boolean} + * Checks if a diagonal bingo is possible + * @param fg {Array>} - the grid with the checked (submitted) values + * @returns {boolean|boolean|*} */ -function checkBingo(bingoGrid) { - let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted)); - - let diagonalBingo = true; +function checkBingoDiagnoal(fg) { + let bingoCheck = true; // diagonal check for (let i = 0; i < fg.length; i++) - diagonalBingo = fg[i][i] && diagonalBingo; - if (diagonalBingo) { - bingoGrid.bingo = true; + bingoCheck = fg[i][i] && bingoCheck; + if (bingoCheck) return true; - } - diagonalBingo = true; + bingoCheck = true; for (let i = 0; i < fg.length; i++) - diagonalBingo = fg[i][fg.length - i - 1] && diagonalBingo; - if (diagonalBingo) { - bingoGrid.bingo = true; - return true; - } + bingoCheck = fg[i][fg.length - i - 1] && bingoCheck; + return bingoCheck; +} + +/** + * Checks if a vertical bingo is possible + * @param fg {Array>} - the grid with the checked (submitted) values + * @returns {boolean|boolean|*} + */ +function checkBingoVertical(fg) { let bingoCheck = true; - // horizontal check for (let row of fg) { bingoCheck = true; for (let field of row) bingoCheck = field && bingoCheck; - if (bingoCheck) { - bingoGrid.bingo = true; + if (bingoCheck) return true; - } - } - if (bingoCheck) { - bingoGrid.bingo = true; - return true; } - bingoCheck = true; + return bingoCheck; +} + +/** + * Checks if a horizontal bingo is possible + * @param fg {Array>} - the grid with the checked (submitted) values + * @returns {boolean|boolean|*} + */ +function checkBingoHorizontal(fg) { + let bingoCheck = true; // vertical check for (let i = 0; i < fg.length; i++) { bingoCheck = true; for (let j = 0; j < fg.length; j++) bingoCheck = fg[j][i] && bingoCheck; - if (bingoCheck) { - bingoGrid.bingo = true; + if (bingoCheck) return true; - } } - if (bingoCheck) { + return bingoCheck; +} + +/** + * Checks if a bingo exists in the bingo grid. + * @param bingoGrid {BingoGrid} + * @returns {boolean} + */ +function checkBingo(bingoGrid) { + let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted)); + let diagonalBingo = checkBingoDiagnoal(fg); + if (diagonalBingo) { + bingoGrid.bingo = true; + return true; + } + let verticalCheck = checkBingoVertical(fg); + + if (verticalCheck) { + bingoGrid.bingo = true; + return true; + } + let horizontalCheck = checkBingoHorizontal(fg); + + if (horizontalCheck) { bingoGrid.bingo = true; return true; } @@ -294,9 +314,9 @@ function checkBingo(bingoGrid) { // -- Router stuff router.use((req, res, next) => { - if (!req.session.bingoUser) { + if (!req.session.bingoUser) req.session.bingoUser = new BingoUser(); - } + next(); }); @@ -311,9 +331,9 @@ router.get('/', (req, res) => { if (!bingoSession.users[bingoUser.id]) bingoSession.addUser(bingoUser); - if (!bingoUser.grids[gameId]) { + 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, @@ -340,7 +360,7 @@ router.graphqlResolver = (req, res) => { return bingoSession; }, checkBingo: () => { - return checkBingo(bingoUser.grids[gameId]) + return checkBingo(bingoUser.grids[gameId]); }, activeGrid: () => { return bingoUser.grids[gameId]; @@ -348,7 +368,7 @@ router.graphqlResolver = (req, res) => { // mutation createGame: ({input}) => { let words = input.words.filter((el) => { // remove empty strings and non-types from word array - return (!!el && el.length > 0) + return (!!el && el.length > 0); }); let size = input.size; if (words.length > 0 && size < 10 && size > 0) { @@ -404,14 +424,14 @@ router.graphqlResolver = (req, res) => { } }, createFollowupGame: () => { - if (bingoSession) { + if (bingoSession) if (!bingoSession.followup) return bingoSession.createFollowup(); else return bingoSessions[bingoSession.followup]; - } else { + else res.status(400); - } + }, sendChatMessage: ({input}) => { input.message = replaceTagSigns(input.message).substring(0, 250); diff --git a/routes/index.js b/routes/index.js index 6aab441..f4f0679 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); /* GET home page. */ -router.get('/', function(req, res, next) { +router.get('/', function(req, res) { res.render('index', { title: 'Trivernis.net' }); }); diff --git a/routes/riddle.js b/routes/riddle.js index 6d23413..ab26927 100644 --- a/routes/riddle.js +++ b/routes/riddle.js @@ -18,10 +18,9 @@ class RedditDownload { /** * Generates an id for a subreddit download. - * @param subreddit * @returns {string} */ -function generateDownloadId(subreddit) { +function generateDownloadId() { return Date.now().toString(16); } @@ -32,7 +31,7 @@ function generateDownloadId(subreddit) { */ function startDownload(subreddit) { if (rWordOnly.test(subreddit)) { - let downloadId = generateDownloadId(subreddit); + let downloadId = generateDownloadId(); let dlFilePath = `./public/static/${downloadId}.zip`; let dlWebPath = `/static/${downloadId}.zip`; let dl = new RedditDownload(dlWebPath); @@ -40,13 +39,11 @@ function startDownload(subreddit) { dl.process = cproc.exec(`python3 -u riddle.py -o ../../public/static/${downloadId} -z --lzma ${subreddit}`, {cwd: './scripts/reddit-riddle', env: {PYTHONIOENCODING: 'utf-8', PYTHONUNBUFFERED: true}}, (err, stdout) => { - if (err) { + if (err) console.error(err); - } else { + else console.log(`riddle.py: ${stdout}`); - } }); - dl.process.on('exit', (code) => { if (code === 0) dl.status = 'finished'; @@ -57,20 +54,17 @@ function startDownload(subreddit) { delete downloads[downloadId]; }, 300000); // delete the file after 5 minutes }); - dl.process.on('message', (msg) => { - console.log(msg) + console.log(msg); }); - downloads[downloadId] = dl; - return downloadId; } } router.use('/files', express.static('./tmp')); -router.get('/', (req, res, next) => { +router.get('/', (req, res) => { res.render('riddle'); }); @@ -84,15 +78,14 @@ router.post('/', (req, res) => { let id = req.body.id; let download = downloads[id]; - if (download) { + if (download) res.send({ id: id, status: download.status, file: download.file }); - } else { + else res.send({error: 'Unknown download ID', id: id}); - } } }); diff --git a/routes/users.js b/routes/users.js index f15a20d..a05b0ad 100644 --- a/routes/users.js +++ b/routes/users.js @@ -2,8 +2,8 @@ const express = require('express'); const router = express.Router(); /* GET users listing. */ -router.get('/', function(req, res, next) { - res.send('respond with a resource'); +router.get('/', function(req, res) { + res.send('There are no users :('); }); module.exports = router; From f026cd6cca7d82e57b4b100bf48aea6630e3d81b Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 12 May 2019 20:44:53 +0200 Subject: [PATCH 25/42] Added Codefactor Badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b99c27..7585627 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# whooshy [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) +# whooshy [![CodeFactor](https://www.codefactor.io/repository/github/trivernis/whooshy/badge/develop)](https://www.codefactor.io/repository/github/trivernis/whooshy/overview/develop) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) This repository is the node.js webserver running on `(beta.)trivernis.net`. From 70fa701fda375647a53f3e0e158becdbc03f47fa Mon Sep 17 00:00:00 2001 From: Trivernis Date: Mon, 13 May 2019 22:22:45 +0200 Subject: [PATCH 26/42] Started migration to database - added table creation script for bingo - added sql scripts for bingo - added data management class for bingo - added libs with utils and global variables --- CHANGELOG.md | 4 + app.js | 166 +++++++------- graphql/bingo.graphql | 94 ++++---- lib/globals.js | 15 ++ lib/utils.js | 39 ++++ routes/bingo.js | 265 ++++++++++++++++++++++- sql/bingo/clearExpired.sql | 50 +++++ sql/bingo/createBingoTables.sql | 69 ++++++ sql/bingo/queries.yaml | 107 +++++++++ sql/{createSessionTable.sql => init.sql} | 7 + 10 files changed, 687 insertions(+), 129 deletions(-) create mode 100644 lib/globals.js create mode 100644 lib/utils.js create mode 100644 sql/bingo/clearExpired.sql create mode 100644 sql/bingo/createBingoTables.sql create mode 100644 sql/bingo/queries.yaml rename sql/{createSessionTable.sql => init.sql} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6f65e..1e50695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - sql-file directory `sql` - LICENSE.md (GPL v3) - eslint to dev dependencys +- table creation script for bingo +- sql scripts for bingo +- data management class for bingo +- libs with utils and global variables ## Changed diff --git a/app.js b/app.js index 858aac3..771587b 100644 --- a/app.js +++ b/app.js @@ -1,106 +1,98 @@ const createError = require('http-errors'), - express = require('express'), - path = require('path'), - cookieParser = require('cookie-parser'), - logger = require('morgan'), - compileSass = require('express-compile-sass'), - session = require('express-session'), - pg = require('pg'), - pgSession = require('connect-pg-simple')(session), - fsx = require('fs-extra'), - yaml = require('js-yaml'), - graphqlHTTP = require('express-graphql'), - { buildSchema } = require('graphql'), - { importSchema } = require('graphql-import'), + express = require('express'), + path = require('path'), + cookieParser = require('cookie-parser'), + logger = require('morgan'), + compileSass = require('express-compile-sass'), + session = require('express-session'), + pgSession = require('connect-pg-simple')(session), + fsx = require('fs-extra'), + graphqlHTTP = require('express-graphql'), + {buildSchema} = require('graphql'), + {importSchema} = require('graphql-import'), - indexRouter = require('./routes/index'), - usersRouter = require('./routes/users'), - riddleRouter = require('./routes/riddle'), - bingoRouter = require('./routes/bingo'); + globals = require('./lib/globals'), + settings = globals.settings, -let settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); + indexRouter = require('./routes/index'), + usersRouter = require('./routes/users'), + riddleRouter = require('./routes/riddle'), + bingoRouter = require('./routes/bingo'); -if (fsx.existsSync('config.yaml')) - Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); async function init() { - // grapql default resolver - let graphqlResolver = (request, response) => { - return { - time: Date.now(), - bingo: bingoRouter.graphqlResolver(request, response) + // grapql default resolver + let graphqlResolver = async (request, response) => { + return { + time: Date.now(), + bingo: await bingoRouter.graphqlResolver(request, response) + }; }; - }; - // database setup - let pgPool = new pg.Pool({ - host: settings.postgres.host, - port: settings.postgres.port, - user: settings.postgres.user, - password: settings.postgres.password, - database: settings.postgres.database - }); - await pgPool.query(fsx.readFileSync('./sql/createSessionTable.sql', 'utf-8')); + // database setup + let pgPool = globals.pgPool; + await pgPool.query(fsx.readFileSync('./sql/init.sql', 'utf-8')); + await bingoRouter.init(); - let app = express(); + let app = express(); - // view engine setup - app.set('views', path.join(__dirname, 'views')); - app.set('view engine', 'pug'); - app.set('trust proxy', 1); + // view engine setup + app.set('views', path.join(__dirname, 'views')); + app.set('view engine', 'pug'); + app.set('trust proxy', 1); - app.use(logger('dev')); - app.use(express.json()); - app.use(express.urlencoded({ extended: false })); - app.use(cookieParser()); - app.use(session({ - store: new pgSession({ - pool: pgPool, - tableName: 'user_sessions' - }), - secret: settings.sessions.secret, - resave: false, - saveUninitialized: true, - cookie: { - maxAge: 30 * 24 * 60 * 60 * 1000 // maxAge 30 days - } - })); - app.use('/sass', compileSass({ - root: './public/stylesheets/sass', - sourceMap: true, - watchFiles: true, - logToConsole: true - })); - app.use(express.static(path.join(__dirname, 'public'))); + app.use(logger('dev')); + app.use(express.json()); + app.use(express.urlencoded({extended: false})); + app.use(cookieParser()); + app.use(session({ + store: new pgSession({ + pool: pgPool, + tableName: 'user_sessions' + }), + secret: settings.sessions.secret, + resave: false, + saveUninitialized: true, + cookie: { + maxAge: 7 * 24 * 60 * 60 * 1000 // maxAge 7 days + } + })); + app.use('/sass', compileSass({ + root: './public/stylesheets/sass', + sourceMap: true, + watchFiles: true, + logToConsole: true + })); + app.use(express.static(path.join(__dirname, 'public'))); - app.use('/', indexRouter); - app.use('/users', usersRouter); - app.use(/\/riddle(\/.*)?/, riddleRouter); - app.use('/bingo', bingoRouter); - app.use('/graphql', graphqlHTTP((request, response) => { - return { - schema: buildSchema(importSchema('./graphql/schema.graphql')), - rootValue: graphqlResolver(request, response), - context: {session: request.session}, - graphiql: true - }; - })); + app.use('/', indexRouter); + app.use('/users', usersRouter); + app.use(/\/riddle(\/.*)?/, riddleRouter); + app.use('/bingo', bingoRouter); + app.use('/graphql', graphqlHTTP(async (request, response) => { + return await { + schema: buildSchema(importSchema('./graphql/schema.graphql')), + rootValue: await graphqlResolver(request, response), + context: {session: request.session}, + graphiql: true + }; + })); // catch 404 and forward to error handler - app.use(function(req, res, next) { - next(createError(404)); - }); + app.use(function (req, res, next) { + next(createError(404)); + }); // error handler - app.use(function(err, req, res) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + app.use(function (err, req, res) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; - // render the error page - res.status(err.status || 500); - res.render('error'); - }); - return app; + // render the error page + res.status(err.status || 500); + res.render('error'); + }); + return app; } module.exports = init; diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 36ec33a..fd2b2fb 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -1,114 +1,128 @@ type BingoMutation { + "creates a lobby for a game and returns the lobby id" + createLobby: ID - # creates a game of bingo and returns the game id + "joins a lobby and returns the connection" + joinLobby(input: joinLobbyInput): PlayerLobbyConnection + + "creates a game of bingo and returns the game" createGame(input: CreateGameInput!): BingoGame - # submit a bingo to the active game session + "submit a bingo to the active game session" submitBingo: BingoGame - # toggle a word (heared or not) on the sessions grid + "toggle a word (heared or not) on the sessions grid" toggleWord(input: WordInput!): BingoGrid - # set the username of the current session + "set the username of the current session" setUsername(input: UsernameInput!): BingoUser - # recreates the active game to a follow-up + "recreates the active game to a follow-up" createFollowupGame: BingoGame - # sends a message to the current sessions chat + "sends a message to the current sessions chat" sendChatMessage(input: MessageInput!): ChatMessage } type BingoQuery { - # Returns the currently active bingo game + "returns the currently active bingo game" gameInfo(input: IdInput): BingoGame - # If there is a bingo in the fields. + "if there is a bingo in the fields." checkBingo: Boolean - # Returns the grid of the active bingo game + "returns the grid of the active bingo game" activeGrid: BingoGrid } +type PlayerLobbyConnection { + + "the id of the player" + playerId: ID! + + "the id of the lobby" + lobbyId: ID! +} + type BingoGame { - # the id of the bingo game + "the id of the bingo game" id: ID! - # the words used in the bingo game + "the words used in the bingo game" words: [String]! - # the size of the square-grid + "the size of the square-grid" gridSize: Int - # an array of players active in the bingo game + "an array of players active in the bingo game" players(input: IdInput): [BingoUser] # the player-ids that scored a bingo bingos: [String]! - # if the game has already finished + "if the game has already finished" finished: Boolean - # the id of the followup game if it has been created + "the id of the followup game if it has been created" followup: ID - # Returns the last n chat-messages + "returns the last n chat-messages" getMessages(input: MessageQueryInput): [ChatMessage!] } type BingoUser { - # the id of the bingo user + "the id of the bingo user" id: ID! - # the id of the currently active bingo game + "the id of the currently active bingo game" game: ID - # the name of the user + "the name of the user" username: String } type BingoGrid { - # the grid represented as string matrix + "the grid represented as string matrix" wordGrid: [[String]]! - # the grid represented as bingo field matrix + "the grid represented as bingo field matrix" fieldGrid: [[BingoField]]! - # if there is a bingo + "if there is a bingo" bingo: Boolean } type BingoField { - # the word contained in the bingo field + "the word contained in the bingo field" word: String - # if the word was already heared + "if the word was already heared" submitted: Boolean! - # the base64 encoded word + "the base64 encoded word" base64Word: String } type ChatMessage { - # the id of the message + "the id of the message" id: ID! - # the content of the message + "the content of the message" content: String! # the content of the message rendered by markdown-it htmlContent: String - # the type of the message + "the type of the message" type: MessageType! - # the username of the sender + "the username of the sender" username: String # the time the message was send (in milliseconds) @@ -119,18 +133,24 @@ type ChatMessage { # input Types # # # +input joinLobbyInput { + + "the id of the lobby to join" + lobbyId: ID! +} + input CreateGameInput { - # the words used to fill the bingo grid + "the words used to fill the bingo grid" words: [String!]! - # the size of the bingo grid + "the size of the bingo grid" size: Int! = 3 } input WordInput { - # the normal word string + "the normal word string" word: String # the base64-encoded word @@ -139,28 +159,28 @@ input WordInput { input UsernameInput { - # the username string + "the username string" username: String! } input IdInput { - # the id + "the id" id: ID! } input MessageInput { - # the message + "the message" message: String! } input MessageQueryInput { - # search for a specific id + "search for a specific id" id: ID - # get the last n messages + "get the last n messages" last: Int = 10 } diff --git a/lib/globals.js b/lib/globals.js new file mode 100644 index 0000000..29cb7ad --- /dev/null +++ b/lib/globals.js @@ -0,0 +1,15 @@ +const utils = require('./utils'), + pg = require('pg'); + +const settings = utils.readSettings('.'); + +Object.assign(exports, { + settings: settings, + pgPool: new pg.Pool({ + host: settings.postgres.host, + port: settings.postgres.port, + user: settings.postgres.user, + password: settings.postgres.password, + database: settings.postgres.database + }) +}); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..dbc8264 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,39 @@ +const yaml = require('js-yaml'), + fsx = require('fs-extra'); + + +/** + * Parses the `queries.yaml` file in the path. queries.yaml-format: + * exports: {List} - query keys to export + * + * queryKey: + * file: {String} name of sql-file if the sql is stored in a file. + * sql: {String} pure sql if it is not stored in a file. Will be replaced by file contents if a file was given. + * @param path {String} - the path where the queries.yaml file is stored + */ +function parseSqlYaml(path) { + let queries = yaml.safeLoad(fsx.readFileSync(`${path}/queries.yaml`)); + + for (let query of queries.exports) + if (queries[query].file) + queries[query].sql = fsx.readFileSync(`${path}/${queries[query].file}`, 'utf-8'); + + return queries; +} + +/** + * Reads the default-config.yaml and config.yaml in the path directory. + * @param path {String} - the directory of the settings files. + */ +function readSettings(path) { + let settings = yaml.safeLoad(fsx.readFileSync(`${path}/default-config.yaml`)); + + if (fsx.existsSync('config.yaml')) + Object.assign(settings, yaml.safeLoad(fsx.readFileSync(`${path}/config.yaml`))); + return settings; +} + +Object.assign(exports, { + parseSqlYaml: parseSqlYaml, + readSettings: readSettings +}); diff --git a/routes/bingo.js b/routes/bingo.js index fcf0bdf..76ce2a5 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -6,10 +6,227 @@ const express = require('express'), md = require('markdown-it')() .use(mdEmoji) .use(mdMark) - .use(mdSmartarrows); + .use(mdSmartarrows), + utils = require('../lib/utils'), + globals = require('../lib/globals'); +let pgPool = globals.pgPool; let bingoSessions = {}; +/** + * Class to manage the bingo data in the database. + */ +class BingoDataManager { + /** + * constructor functino + * @param postgresPool {pg.Pool} - the postgres pool + */ + constructor(postgresPool) { + this.pgPool = postgresPool; + this.queries = utils.parseSqlYaml('./sql/bingo') + } + + async init() { + await this.pgPool.query(this.queries.createTables.sql); + setInterval(async () => await this._databaseCleanup(), 5*60*1000); // database cleanup every 5 minutes + } + + /** + * Try-catch wrapper around the pgPool.query. + * @param query {String} - the sql query + * @param [values] {Array} - an array of values + * @returns {Promise<*>} + */ + async _queryDatabase(query, values) { + try { + return await this.pgPool.query(query, values); + } catch (err) { + console.error(`Error on query "${query}" with values ${JSON.stringify(values)}.`); + console.error(err); + console.error(err.stack); + return { + rows: null + }; + } + } + + /** + * Queries the database and returns all resulting rows + * @param query {String} - the sql query + * @param values {Array} - an array of parameters needed in the query + * @returns {Promise<*>} + * @private + */ + async _queryAllResults(query, values) { + let result = await this._queryDatabase(query, values); + return result.rows; + } + + /** + * Query the database and return the first result or null + * @param query {String} - the sql query + * @param values {Array} - an array of parameters needed in the query + * @returns {Promise<*>} + */ + async _queryFirstResult(query, values) { + let result = await this._queryDatabase(query, values); + if (result.rows.length > 0) + return result.rows[0]; + } + + /** + * Clears expired values from the database. + */ + async _databaseCleanup() { + await this._queryDatabase(this.queries.cleanup.sql); + } + + /** + * Add a player to the players table + * @param username {String} - the username of the player + * @returns {Promise<*>} + */ + async addPlayer(username) { + let result = await this._queryFirstResult(this.queries.addPlayer.sql, [username]); + if (result) + return result; + else + return {}; // makes things easier + } + + /** + * Updates the username of a player + * @param playerId {Number} - the id of the player + * @param username {String} - the new username + * @returns {Promise} + */ + async updatePlayerUsername(playerId, username) { + return await this._queryFirstResult(this.queries.updatePlayerUsername, [username, playerId]); + } + + /** + * Returns the username for a player-id + * @param playerId {Number} - the id of the player + * @returns {Promise<*>} + */ + async getPlayerUsername(playerId) { + let result = await this._queryFirstResult(this.queries.getPlayerUsername, [playerId]); + if (result) + return result.username; + } + + /** + * Updates the expiration date of a player + * @param playerId {Request} - thie id of the player + * @returns {Promise} + */ + async updatePlayerExpiration(playerId) { + await this._queryDatabase(this.queries.updatePlayerExpire.sql, [playerId]); + } + + /** + * Creates a bingo lobby. + * @param playerId + * @param gridSize + * @returns {Promise<*>} + */ + async createLobby(playerId, gridSize) { + return await this._queryFirstResult(this.queries.addLobby.sql, [playerId, gridSize]); + } + + /** + * Updates the expiration date of a lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async updateLobbyExpiration(lobbyId) { + return await this._queryDatabase(this.queries.updateLobbyExpire.sql, [lobbyId]); + } + + /** + * Checks if a player is in a lobby. + * @param playerId {Number} - the id of the player + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async getPlayerInLobby(playerId, lobbyId) { + return (await this._queryFirstResult(this.queries.getPlayerInLobby.sql, [playerId, lobbyId])); + } + + /** + * Adds a player to a lobby. + * @param playerId {Number} - the id of the player + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async addPlayerToLobby(playerId, lobbyId) { + let entry = await this.getPlayerInLobby(playerId, lobbyId); + if (entry) + return entry; + else + return await this._queryFirstResult(this.queries.addPlayerToLobby.sql, [playerId, lobbyId]); + } + + /** + * Removes a player from a lobbby + * @param playerId {Number} - the id of the player + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async removePlayerFromLobby(playerId, lobbyId) { + return await this._queryFirstResult(this.queries.removePlayerFromLobby.sql, [playerId, lobbyId]); + } + + /** + * Adds a word to a lobby + * @param lobbyId {Number} - the id of the lobby + * @param word {Number} - the id of the word + * @returns {Promise} + */ + async addWordToLobby(lobbyId, word) { + return await this._queryFirstResult(this.queries.addWord.sql, [lobbyId, word]); + } + + /** + * Returns all words used in a lobby + * @param lobbyId + * @returns {Promise} + */ + async getWordsForLobby(lobbyId) { + return await this._queryAllResults(this.queries.getWordsForLobby.sql, [lobbyId]); + } + + /** + * Adds a grid for a user to a lobby + * @param lobbyId {Number} - the id of the lobby + * @param playerId {Number} - the id of the user + * @returns {Promise} + */ + async addGrid(lobbyId, playerId) { + return await this._queryFirstResult(this.queries.addGrid.sql, [playerId, lobbyId]); + } + + /** + * Adds a word to a grid with specific location + * @param gridId {Number} - the id of the gird + * @param wordId {Number} - the id of the word + * @param row {Number} - the number of the row + * @param column {Number} - the number of the column + */ + async addWordToGrid(gridId, wordId, row, column) { + return await this._queryFirstResult(this.queries.addWordToGrid.sql, [gridId, wordId, row, column]); + } + + /** + * Returns all words in the grid with location + * @param gridId {Number} - the id of the grid + * @returns {Promise<*>} + */ + async getWordsInGrid(gridId) { + return await this._queryAllResults(this.queries.getWordsInGrid.sql, [gridId]); + } +} + class BingoSession { /** * constructor @@ -311,12 +528,21 @@ function checkBingo(bingoGrid) { return false; } + // -- Router stuff -router.use((req, res, next) => { + +let bdm = new BingoDataManager(pgPool); + +router.init = async () => { + await bdm.init(); +}; + +router.use(async (req, res, next) => { if (!req.session.bingoUser) req.session.bingoUser = new BingoUser(); - + if (req.session.bingoPlayerId) + await bdm.updatePlayerExpiration(req.session.bingoPlayerId); next(); }); @@ -347,10 +573,13 @@ router.get('/', (req, res) => { } }); -router.graphqlResolver = (req, res) => { +router.graphqlResolver = async (req, res) => { + if (req.session.bingoPlayerId) + await bdm.updatePlayerExpiration(req.session.bingoPlayerId); let bingoUser = req.session.bingoUser || new BingoUser(); let gameId = req.query.game || bingoUser.game || null; let bingoSession = bingoSessions[gameId]; + return { // queries gameInfo: ({input}) => { @@ -366,6 +595,28 @@ router.graphqlResolver = (req, res) => { return bingoUser.grids[gameId]; }, // mutation + createLobby: async ({input}) => { + let gridSize = (input && input.gridSize)? input.gridSize : 3; + let lobby = await bdm.createLobby(req.session.bingoPlayerId, gridSize); + if (lobby && lobby.id) + return lobby.id; + else + res.status(500); + }, + joinLobby: async ({input}) => { + if (input.lobbyId) { + let entry = await bdm.addPlayerToLobby(req.session.bingoPlayerId, input.lobbyId); + if (entry && entry.lobby_id && entry.player_id) + return { + lobbyId: entry.lobby_id, + playerId: entry.player_id + }; + else + res.status(500); + } else { + res.status(400); + } + }, createGame: ({input}) => { let words = input.words.filter((el) => { // remove empty strings and non-types from word array return (!!el && el.length > 0); @@ -413,10 +664,14 @@ router.graphqlResolver = (req, res) => { res.status(400); } }, - setUsername: ({input}) => { + setUsername: async ({input}) => { if (input.username) { bingoUser.username = input.username.substring(0, 30); // only allow 30 characters + if (!req.session.bingoPlayerId) + req.session.bingoPlayerId = (await bdm.addPlayer(input.username)).id; + else + await bdm.updatePlayerUsername(req.session.bingoPlayerId, input.username); if (bingoSession) bingoSession.addUser(bingoUser); diff --git a/sql/bingo/clearExpired.sql b/sql/bingo/clearExpired.sql new file mode 100644 index 0000000..9a0fb9a --- /dev/null +++ b/sql/bingo/clearExpired.sql @@ -0,0 +1,50 @@ +/*-- remove grid-word connections for expired lobbys +DELETE FROM bingo.grid_words +WHERE EXISTS( + SELECT grids.lobby_id FROM bingo.grids + WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = grids.lobby_id + AND NOW() > lobbys.expire + ) +); + +-- remove grids for expired lobbys +DELETE FROM bingo.grids +WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = grids.lobby_id + AND NOW() > lobbys.expire +); + +-- remove words for expired lobbys +DELETE FROM bingo.words +WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = words.lobby_id + AND NOW() > lobbys.expire +); + +-- remove lobby-player connections for expired lobbys or players +DELETE FROM bingo.lobby_players +WHERE EXISTS ( + SELECT lobbys.id FROM bingo.lobbys + WHERE lobbys.id = lobby_players.lobby_id + AND NOW() > lobbys.expire +) OR EXISTS ( + SELECT players.id FROM bingo.players + WHERE players.id = lobby_players.player_id + AND NOW() > players.expire +); +*/ +-- remove expired lobbys +DELETE FROM bingo.lobbys +WHERE NOW() > lobbys.expire; + +-- remove expired players +DELETE FROM bingo.players +WHERE NOW() > players.expire; +/*AND NOT EXISTS ( + SELECT lobbys.admin_id FROM bingo.lobbys + WHERE lobbys.admin_id = players.id +);*/ diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql new file mode 100644 index 0000000..dd3c193 --- /dev/null +++ b/sql/bingo/createBingoTables.sql @@ -0,0 +1,69 @@ +-- players table +CREATE TABLE IF NOT EXISTS bingo.players ( + id serial UNIQUE PRIMARY KEY, + username varchar(32) NOT NULL, + expire timestamp DEFAULT (NOW() + interval '24 hours' ) +); + +-- lobbys table +CREATE TABLE IF NOT EXISTS bingo.lobbys ( + id serial UNIQUE PRIMARY KEY, + admin_id serial references bingo.players(id) ON DELETE SET NULL, + grid_size integer DEFAULT 3, + expire timestamp DEFAULT (NOW() + interval '1 hour' ) +); + +-- lobbys-players table +CREATE TABLE IF NOT EXISTS bingo.lobby_players ( + player_id serial references bingo.players(id) ON DELETE CASCADE, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + score integer DEFAULT 0, + PRIMARY KEY (player_id, lobby_id) +); + +-- words table +CREATE TABLE IF NOT EXISTS bingo.words ( + id serial UNIQUE PRIMARY KEY, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + heared integer DEFAULT 0, + content varchar(254) NOT NULL +); + +-- grids table +CREATE TABLE IF NOT EXISTS bingo.grids ( + id serial UNIQUE PRIMARY KEY, + player_id serial references bingo.players(id) ON DELETE CASCADE, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE +); + +-- grids_words table +CREATE TABLE IF NOT EXISTS bingo.grid_words ( + grid_id serial references bingo.grids(id) ON DELETE CASCADE, + word_id serial references bingo.words(id) ON DELETE CASCADE, + grid_row integer NOT NULL, + grid_column integer NOT NULL, + PRIMARY KEY (grid_id, word_id) +); + +-- messages table +CREATE TABLE IF NOT EXISTS bingo.messages ( + id serial UNIQUE PRIMARY KEY, + content varchar(255) NOT NULL, + player_id serial references bingo.players(id) ON DELETE SET NULL, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + type varchar(8), + created timestamp DEFAULT NOW() +); + +-- rounds table +CREATE TABLE IF NOT EXISTS bingo.rounds ( + id serial UNIQUE PRIMARY KEY, + start timestamp DEFAULT NOW(), + finish timestamp, + status varchar(8) DEFAULT 'undefined', + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + winner serial references bingo.players(id) ON DELETE SET NULL +); + +-- altering +ALTER TABLE bingo.lobbys ADD COLUMN IF NOT EXISTS current_round serial references bingo.rounds(id) ON DELETE SET NULL; diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml new file mode 100644 index 0000000..d8e3efa --- /dev/null +++ b/sql/bingo/queries.yaml @@ -0,0 +1,107 @@ +# a file to list sql queries by names + +exports: # loaded from file + - createTables + - cleanup + +# create the needed bingo tables +createTables: + file: createBingoTables.sql + +# clears expired values +cleanup: + file: clearExpired.sql + +# Add a player to the database +# params: +# - {String} - the username of the player +addPlayer: + sql: INSERT INTO bingo.players (username) VALUES ($1) RETURNING *; + +# Updates the username of a player +# params: +# - {String} - the new username of the player +# - {Number} - the id of the player +updatePlayerUsername: + sql: UPDATE bingo.players SET players.username = $1 WHERE players.id = $2 RETURNING *; + +# Selects the username for a player id +# params: +# - {Number} - the id of the player +getPlayerUsername: + sql: SELECT players.username FROM bingo.players WHERE id = $1; + +# updates the expiration timestamp of the player +# params: +# - {Number} - the id of the player +updatePlayerExpire: + sql: UPDATE bingo.players SET expire = (NOW() + interval '24 hours') WHERE id = $1; + +# adds a lobby to the database +# params: +# - {Number} - the id of the admin player +# - {Number} - the size of the grid +addLobby: + sql: INSERT INTO bingo.lobbys (admin_id, grid_size) VALUES ($1, $2) RETURNING *; + +# updates expiration timestamp of the lobby +# params: +# - {Number} - the id of the lobby +updateLobbyExpire: + sql: UPDATE bingo.lobbys SET expire = (NOW() + interval '1 hours') WHERE id = $1; + +# inserts a player into a lobby +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +addPlayerToLobby: + sql: INSERT INTO bingo.lobby_players (player_id, lobby_id) VALUES ($1, $2); + +# removes a player from a lobby +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +removePlayerFromLobby: + sql: REMOVE FROM bingo.lobby_players WHERE player_id = $1 AND lobby_id = $2; + +# returns the entry of the player and lobby +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +getPlayerInLobby: + sql: SELECT * FROM bingo.lobby_players lp WHERE lp.player_id = $1 AND lp.lobby_id = $2; + +# adds a word to the database +# params: +# - {Number} - the id of the lobby where the word is used +# - {String} - the word itself +addWord: + sql: INSERT INTO bingo.words (lobby_id, content) VALUES ($1, $2) RETURNING *; + +# returns all words for a bingo game (lobby) +# params: +# - {Number} - the id of the bingo lobby +getWordsForLobby: + sql: SELECT * FROM bingo.words WHERE words.lobby_id = $1; + +# adds a grid to the database +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +addGrid: + sql: INSERT INTO bingo.grids (player_id, lobby_id) VALUES ($1, $2) RETURNING *; + +# inserts grid-word connections into the database +# params: +# - {Number} - the id of the grid +# - {Number} - the id of the word +# - {Number} - the row of the word +# - {Number} - the column of the word +addWordToGrid: + sql: INSERT INTO bingo.grid_words (grid_id, word_id, grid_row, grid_column) VALUES ($1, $2, $3, $4) RETURNING *; + +# returns all words for a players grid +# params: +# - {Number} - the id of the grid +getWordsForGridId: + sql: SELECT * FROM bingo.grid_words, bingo.words WHERE grid_words.grid_id = $1 AND words.id = grid_words.word_id; diff --git a/sql/createSessionTable.sql b/sql/init.sql similarity index 70% rename from sql/createSessionTable.sql rename to sql/init.sql index 50b1234..fbbfd62 100644 --- a/sql/createSessionTable.sql +++ b/sql/init.sql @@ -1,3 +1,10 @@ +-- schemas + +CREATE SCHEMA IF NOT EXISTS bingo; + +-- public tables + +-- creates the Session table CREATE TABLE IF NOT EXISTS "user_sessions" ( "sid" varchar NOT NULL COLLATE "default", "sess" json NOT NULL, From 9b31ac80e094f81c83b6ba98ae457000fb7877fb Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 14 May 2019 19:35:34 +0200 Subject: [PATCH 27/42] Broken frontend, working backend - completed backend database migration - added backend-functions and changed structure - frontend broken at this moment --- graphql/bingo.graphql | 236 ++++--- package.json | 7 +- routes/bingo.js | 1116 +++++++++++++++++++++++-------- sql/bingo/createBingoTables.sql | 47 +- sql/bingo/queries.yaml | 148 +++- 5 files changed, 1140 insertions(+), 414 deletions(-) diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index fd2b2fb..fa3d9df 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -1,111 +1,162 @@ type BingoMutation { - "creates a lobby for a game and returns the lobby id" - createLobby: ID - "joins a lobby and returns the connection" - joinLobby(input: joinLobbyInput): PlayerLobbyConnection + "creates a lobby for a game and returns the lobby" + createLobby(gridSize: Int = 3): BingoLobby - "creates a game of bingo and returns the game" - createGame(input: CreateGameInput!): BingoGame + "lobby mutations" + mutateLobby(id: ID!): LobbyMutation - "submit a bingo to the active game session" - submitBingo: BingoGame + "sets the username of a player" + setUsername(username: String): BingoPlayer +} - "toggle a word (heared or not) on the sessions grid" - toggleWord(input: WordInput!): BingoGrid +type LobbyMutation { - "set the username of the current session" - setUsername(input: UsernameInput!): BingoUser + "joins the lobby" + join: BingoLobby - "recreates the active game to a follow-up" - createFollowupGame: BingoGame + "leaves the lobby" + leave: Boolean - "sends a message to the current sessions chat" - sendChatMessage(input: MessageInput!): ChatMessage -} + "kicks a player from the lobby" + kickPlayer(playerId: ID!): BingoLobby -type BingoQuery { + "starts a round in a lobby if the user is the admin" + startRound: BingoRound + + "sets the new gridsize for the lobby" + setGridSize(gridSize: Int!): BingoLobby - "returns the currently active bingo game" - gameInfo(input: IdInput): BingoGame + "sets the words of a lobby" + setWords(words: [String]): BingoLobby - "if there is a bingo in the fields." - checkBingo: Boolean + "sends a message to the lobby" + sendMessage(message: String): ChatMessage - "returns the grid of the active bingo game" - activeGrid: BingoGrid + "submits bingo" + submitBingo: BingoRound + + "submits a click on a word and returns the field" + toggleGridField(location: GridCoordinates!): GridField } -type PlayerLobbyConnection { +type BingoQuery { - "the id of the player" - playerId: ID! + "returns information about a lobby with a specific id" + lobby(id: ID!): BingoLobby - "the id of the lobby" - lobbyId: ID! + "returns information about a player (if no id is set, its the sessions player)" + player(id: ID): BingoPlayer } -type BingoGame { +type BingoLobby { - "the id of the bingo game" + "the id of the lobby" id: ID! - "the words used in the bingo game" - words: [String]! + "the grid size of the lobby" + gridSize: Int! - "the size of the square-grid" - gridSize: Int + "all players in the lobby" + players: [BingoPlayer] - "an array of players active in the bingo game" - players(input: IdInput): [BingoUser] + "the admin of the lobby" + admin: BingoPlayer - # the player-ids that scored a bingo - bingos: [String]! + "the active bingo round" + currentRound: BingoRound - "if the game has already finished" - finished: Boolean + "all rounds the game had" + rounds: [BingoRound] - "the id of the followup game if it has been created" - followup: ID + "the messages send in the lobby" + messages(limit: Int): [ChatMessage!] - "returns the last n chat-messages" - getMessages(input: MessageQueryInput): [ChatMessage!] + "the words used in the lobby" + words: [BingoWord] } -type BingoUser { +type BingoPlayer { - "the id of the bingo user" + "the id of the player" id: ID! - "the id of the currently active bingo game" - game: ID - - "the name of the user" + "the username of the player" username: String + + "the grid of the player with the given lobby-id" + grid(lobbyId: ID!): BingoGrid + + "the number of wins the user had in the lobby" + wins(lobbyId: ID!): Int +} + +type BingoRound { + "the id of the bingo round" + id: ID! + + "the winner of the round (if exists)" + winner: BingoPlayer + + "start time of the round" + start: String! + + "finish time of the round" + finish: String + + "the status of the bingo round" + status: RoundStatus + + "the lobby of the bingo round" + lobby: BingoLobby } type BingoGrid { - "the grid represented as string matrix" - wordGrid: [[String]]! + "the id of the grid" + id: ID! "the grid represented as bingo field matrix" - fieldGrid: [[BingoField]]! + fields: [GridField]! "if there is a bingo" bingo: Boolean + + "the size of the grid" + size: Int } -type BingoField { +type GridField { "the word contained in the bingo field" - word: String + word: BingoWord "if the word was already heared" submitted: Boolean! - "the base64 encoded word" - base64Word: String + "the row of the field" + row: Int! + + "the column of the field" + column: Int! +} + +type BingoWord { + + "the id of the word" + id: ID! + + "the word itself" + content: String! + + "the number of players that heared this word" + heared: Int! + + "the lobby where the word is used" + lobby: BingoLobby + + "if the word was actually heared" + confirmed: Boolean } type ChatMessage { @@ -116,72 +167,33 @@ type ChatMessage { "the content of the message" content: String! - # the content of the message rendered by markdown-it + "the content rendered by markdown-it" htmlContent: String "the type of the message" type: MessageType! "the username of the sender" - username: String + author: BingoPlayer + + "the lobby where the message was send in" + lobby: BingoLobby - # the time the message was send (in milliseconds) - datetime: String! + "the time the message was created" + created: String! } # # # input Types # # # -input joinLobbyInput { - - "the id of the lobby to join" - lobbyId: ID! -} +input GridCoordinates { -input CreateGameInput { + "the grid row" + row: Int! - "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! -} - -input MessageInput { - - "the message" - message: String! -} - -input MessageQueryInput { - - "search for a specific id" - id: ID - - "get the last n messages" - last: Int = 10 + "the grid column" + column: Int! } # # @@ -193,3 +205,9 @@ enum MessageType { ERROR INFO } + +enum RoundStatus { + ACTIVE + FINISHED + PAUSED +} diff --git a/package.json b/package.json index f5fc6d9..6b1c6db 100644 --- a/package.json +++ b/package.json @@ -67,12 +67,7 @@ "warn", "consistent" ], - "camelcase": [ - "error", - { - "properties": "always" - } - ], + "camelcase": "off", "comma-spacing": [ "error", { diff --git a/routes/bingo.js b/routes/bingo.js index 76ce2a5..5e6785d 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -23,7 +23,7 @@ class BingoDataManager { */ constructor(postgresPool) { this.pgPool = postgresPool; - this.queries = utils.parseSqlYaml('./sql/bingo') + this.queries = utils.parseSqlYaml('./sql/bingo'); } async init() { @@ -59,7 +59,7 @@ class BingoDataManager { */ async _queryAllResults(query, values) { let result = await this._queryDatabase(query, values); - return result.rows; + return result.rows || []; } /** @@ -70,7 +70,7 @@ class BingoDataManager { */ async _queryFirstResult(query, values) { let result = await this._queryDatabase(query, values); - if (result.rows.length > 0) + if (result.rows && result.rows.length > 0) return result.rows[0]; } @@ -101,7 +101,7 @@ class BingoDataManager { * @returns {Promise} */ async updatePlayerUsername(playerId, username) { - return await this._queryFirstResult(this.queries.updatePlayerUsername, [username, playerId]); + return await this._queryFirstResult(this.queries.updatePlayerUsername.sql, [username, playerId]); } /** @@ -109,10 +109,8 @@ class BingoDataManager { * @param playerId {Number} - the id of the player * @returns {Promise<*>} */ - async getPlayerUsername(playerId) { - let result = await this._queryFirstResult(this.queries.getPlayerUsername, [playerId]); - if (result) - return result.username; + async getPlayerInfo(playerId) { + return await this._queryFirstResult(this.queries.getPlayerInfo.sql, [playerId]); } /** @@ -143,6 +141,15 @@ class BingoDataManager { return await this._queryDatabase(this.queries.updateLobbyExpire.sql, [lobbyId]); } + /** + * Returns the row of the lobby. + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async getLobbyInfo(lobbyId) { + return await this._queryFirstResult(this.queries.getLobbyInfo.sql, [lobbyId]); + } + /** * Checks if a player is in a lobby. * @param playerId {Number} - the id of the player @@ -153,6 +160,25 @@ class BingoDataManager { return (await this._queryFirstResult(this.queries.getPlayerInLobby.sql, [playerId, lobbyId])); } + /** + * Returns all players in a lobby. + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async getLobbyPlayers(lobbyId) { + return await this._queryAllResults(this.queries.getLobbyPlayers.sql, [lobbyId]); + } + + /** + * Returns the last messages in a lobby with a limit + * @param lobbyId {Number} - the id of the lobby + * @param [limit] {Number} - the maximum of messages to fetch + * @returns {Promise<*>} + */ + async getLobbyMessages(lobbyId, limit=20) { + return await this._queryAllResults(this.queries.getLobbyMessages.sql, [lobbyId, limit]); + } + /** * Adds a player to a lobby. * @param playerId {Number} - the id of the player @@ -161,10 +187,32 @@ class BingoDataManager { */ async addPlayerToLobby(playerId, lobbyId) { let entry = await this.getPlayerInLobby(playerId, lobbyId); - if (entry) + if (entry) { + entry.lobby_id = entry.id; return entry; - else + } else { return await this._queryFirstResult(this.queries.addPlayerToLobby.sql, [playerId, lobbyId]); + } + } + + /** + * Sets the current round property of the lobby + * @param lobbyId {Number} - the id of the lobby + * @param roundId {Number} - the id of the round + * @returns {Promise<*>} + */ + async setLobbyCurrentRound(lobbyId, roundId) { + return await this._queryFirstResult(this.queries.setLobbyCurrentRound.sql, [lobbyId, roundId]); + } + + /** + * Updates the grid size of a lobby + * @param lobbyId {Number} - the id of the lobby + * @param gridSize {Number} - the new grid size + * @returns {Promise<*>} + */ + async setLobbyGridSize(lobbyId, gridSize) { + return await this._queryFirstResult(this.queries.setLobbyGridSize.sql, [lobbyId, gridSize]); } /** @@ -189,21 +237,69 @@ class BingoDataManager { /** * Returns all words used in a lobby - * @param lobbyId + * @param lobbyId {Number} - the id of the lobby * @returns {Promise} */ async getWordsForLobby(lobbyId) { return await this._queryAllResults(this.queries.getWordsForLobby.sql, [lobbyId]); } + /** + * Returns information about a word + * @param wordId {Number} - the id of the word + * @returns {Promise<*>} + */ + async getWordInfo(wordId) { + return await this._queryFirstResult(this.queries.getWordInfo.sql, [wordId]); + } + + /** + * Returns all rounds of a lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async getLobbyRounds(lobbyId) { + return await this._queryAllResults(this.queries.getLobbyRounds.sql, [lobbyId]); + } + + /** + * Returns the round row of a round + * @param roundId {Number} - the id of the round + * @returns {Promise<*>} + */ + async getRoundInfo(roundId) { + return await this._queryFirstResult(this.queries.getRoundInfo.sql, [roundId]); + } + + /** + * Updates the status of a round + * @param roundId {Number} - the id of the round + * @param status {String<8>} - the new status + * @returns {Promise<*>} + */ + async updateRoundStatus(roundId, status) { + return await this._queryFirstResult(this.queries.updateRoundStatus.sql, [roundId, status]); + } + + /** + * Returns the number of wins the player had in a lobby + * @param lobbyId {Number} - the id of the lobby + * @param playerId {Number} - the id of the player + * @returns {Promise<*>} + */ + async getLobbyPlayerWins(lobbyId, playerId) { + return await this._queryFirstResult(this.queries.getLobbyPlayerWins.sql, [lobbyId, playerId]); + } + /** * Adds a grid for a user to a lobby * @param lobbyId {Number} - the id of the lobby * @param playerId {Number} - the id of the user - * @returns {Promise} + * @param roundId {Number} - the id of the round + * @returns {Promise<*>} */ - async addGrid(lobbyId, playerId) { - return await this._queryFirstResult(this.queries.addGrid.sql, [playerId, lobbyId]); + async addGrid(lobbyId, playerId, roundId) { + return await this._queryFirstResult(this.queries.addGrid.sql, [playerId, lobbyId, roundId]); } /** @@ -223,149 +319,689 @@ class BingoDataManager { * @returns {Promise<*>} */ async getWordsInGrid(gridId) { - return await this._queryAllResults(this.queries.getWordsInGrid.sql, [gridId]); + return await this._queryAllResults(this.queries.getWordsForGridId.sql, [gridId]); + } + + /** + * Returns the grid row for a player and lobby id + * @param lobbyId {Number} - the id of the lobby + * @param playerId {Number} - the id of the player + * @param roundId {Number} - the id of the round + * @returns {Promise<*>} + */ + async getGridForPlayerLobbyRound(lobbyId, playerId, roundId) { + return await this._queryFirstResult(this.queries.getGridByPlayerLobbyRound.sql, [playerId, lobbyId, roundId]); + } + + /** + * Returns the grid row for a grid id + * @param gridId {Number} - the id of the grid + * @returns {Promise<*>} + */ + async getGridInfo(gridId) { + return await this._queryFirstResult(this.queries.getGridInfo.sql, [gridId]); + } + + /** + * Sets a field of the grid to submitted/unsubmitted depending on the previous value + * @param gridId {Number} - the id of the grid + * @param fieldRow {Number} - the row of the field + * @param fieldColumn {Number} - the column of the field + * @returns {Promise<*>} + */ + async toggleGridFieldSubmitted(gridId, fieldRow, fieldColumn) { + return await this._queryFirstResult(this.queries.toggleGridFieldSubmitted.sql, [gridId, fieldRow, fieldColumn]); + } + + /** + * Adds a round for a lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async addRound(lobbyId) { + return await this._queryFirstResult(this.queries.addRound.sql, [lobbyId]); + } + + /** + * Updates a round to the "FINISHED" status and sets the finish time + * @param roundId {Number} + * @returns {Promise<*>} + */ + async setRoundFinished(roundId) { + return await this._queryFirstResult(this.queries.setRoundFinished.sql, [roundId]); + } + + /** + * Updates the rounds winner + * @param winnerId {Number} - the id of the winner + * @returns {Promise<*>} + */ + async setRoundWinner(winnerId) { + return await this._queryFirstResult(this.queries.setRoundWiner.sql, [winnerId]); + } + + /** + * Inserts a message of type "USER" into the database + * @param lobbyId {Number} - the id of the lobby + * @param playerId {Number} - the id of the author (player) + * @param messageContent {String} - the content of the message + * @returns {Promise<*>} + */ + async addUserMessage(lobbyId, playerId, messageContent) { + return await this._queryFirstResult(this.queries.addUserMessage.sql, [playerId, lobbyId, messageContent]); + } + + /** + * Removes all words of a lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async clearLobbyWords(lobbyId) { + return await this._queryFirstResult(this.queries.clearLobbyWords.sql, [lobbyId]); + } + + /** + * Returns a single entry from grid_words + * @param gridId {Number} - the id of the grid + * @param row {Number} - the row of the field + * @param column {Number} - the column of the row + * @returns {Promise<*>} + */ + async getGridField(gridId, row, column) { + return await this._queryFirstResult(this.queries.getGriedField.sql, [gridId, row, column]); } } -class BingoSession { +class WordWrapper { /** * constructor - * @param words List - * @param [size] Number + * @param id {Number} - the id of the word + * @param [row] {Object} - the database row of the word */ - constructor(words, size = 3) { - this.id = generateBingoId(); - this.words = words; - this.gridSize = size; - this.users = {}; - this.bingos = []; // array with the users that already had bingo - this.finished = false; - this.followup = null; - this.chatMessages = []; + constructor(id, row) { + this.id = id; + this._infoLoaded = false; + if (row) + this._assignProperties(row); } /** - * Adds a user to the session - * @param user + * Loads the word information from the database + * @returns {Promise} + * @private */ - addUser(user) { - let id = user.id; - this.users[id] = user; - if (user.username !== 'anonymous') - this.chatMessages.push(new BingoChatMessage(`**${user.username}** joined.`, "INFO")); + async _loadWordInfo() { + if (!this._infoLoaded) { + let row = await bdm.getWordInfo(this.id); + if (row) + this._assignProperties(row); + } + } + /** + * Assign the row as properties + * @param row {Object} - the word row + * @private + */ + _assignProperties(row) { + this._content = row.content; + this._heared = row.heared; + this._lobbyId = row.lobbyId; + this._infoLoaded = this._content && this._heared && this._lobbyId; } /** - * Graphql endpoint - * @param args {Object} - the arguments passed on the graphql interface - * @returns {any[]|*} + * Returns a new lobby wrapper for the words lobby + * @returns {Promise} */ - players(args) { - let input = args? args.input : null; - if (input && input.id) - return [this.users[input.id]]; - else - return Object.values(this.users); + async lobby() { + await this._loadWordInfo(); + return new LobbyWrapper(this._lobbyId); } /** - * Creates a followup BingoSession - * @returns {BingoSession} + * Returns the word iteself + * @returns {Promise} */ - createFollowup() { - let followup = new BingoSession(this.words, this.gridSize); - this.followup = followup.id; - bingoSessions[followup.id] = followup; - followup.chatMessages = this.chatMessages; - followup.chatMessages.push(new BingoChatMessage('==**Rematch**==', "INFO")); - return followup; + async content() { + await this._loadWordInfo(); + return this._content; } /** - * Graphql endpoint to get the last n messages or messages by id - * @param args {Object} - arguments passed by graphql - * @returns {[]} + * Returns the number of people that heared the word + * @returns {Promise} */ - getMessages(args) { - let input = args.input || null; - if (input && input.id) - return this.chatMessages.find(x => (x && x.id === input.id)); - else if (input && input.last) - return this.chatMessages.slice(-input.last); - else - return this.chatMessages.slice(-10); + async heared() { + await this._loadWordInfo(); + return this._heared; + } + /** + * Returns if the word was confirmed heared + * @returns {Promise} + */ + async confirmed() { + await this._loadWordInfo(); + return true; // TODO confirmation logic } +} +class GridFieldWrapper { /** - * Sends the message that a user toggled a word. - * @param base64Word - * @param bingoUser + * constructor + * @param row {Object} - the resulting row */ - sendToggleInfo(base64Word, bingoUser) { - let word = Buffer.from(base64Word, 'base64').toString(); - let toggleMessage = new BingoChatMessage(`**${bingoUser.username}** toggled phrase "${word}".`, "INFO"); - this.chatMessages.push(toggleMessage); + constructor(row) { + this.row = row.grid_row; + this.column = row.grid_column; + this.submitted = row.submitted; + this.word = new WordWrapper(row.word_id, row); + this.grid = new GridWrapper(row.grid_id); } } -class BingoChatMessage { +class GridWrapper { + constructor(id) { + this.id = id; + this._infoLoaded = false; + } + + /** + * Loads all direct grid information + * @returns {Promise} + * @private + */ + async _loadGridInfo() { + if (!this._infoLoaded) { + let result = await bdm.getGridInfo(this.id); + if (result) { + this.playerId = result.player_id; + this.lobbyId = result.lobby_id; + this.size = result.grid_size; + } + } + } + /** - * Chat Message class constructor - * @param messageContent {String} - the messages contents - * @param type {String} - the type constant of the message (USER, ERROR, INFO) - * @param [username] {String} - the username of the user who send this message + * Gets a matrix of the submitted-values of each grid field + * @returns {Promise} + * @private */ - constructor(messageContent, type="USER", username) { - this.id = generateBingoId(); - this.content = messageContent; - this.htmlContent = md.renderInline(messageContent); - this.datetime = Date.now(); - this.username = username; - this.type = type; + async _getSubmittedMatrix() { + await this._loadGridInfo(); + let rows = await bdm.getWordsInGrid(this.id); + let matrix = []; + for (let i = 0; i < this.size; i++) { + matrix[i] = []; + for (let j = 0; j < this.size; j++) + matrix[i][j] = rows.find(x => (x.grid_row === i && x.grid_column === j)).submitted; + } + return matrix; + } + + /** + * Returns all fields in the grid + * @returns {Promise} + */ + async fields() { + let rows = await bdm.getWordsInGrid(this.id); + let fields = []; + for (let row of rows) + fields.push(new GridFieldWrapper(row)); + return fields; + } + + /** + * Returns a field with the current row and column + * @param row {Number} - the fields row + * @param column {Number} - the fields column + * @returns {Promise} + */ + async field({row, column}) { + let result = await bdm.getGridField(this.id, row, column); + return new GridFieldWrapper(result); + } + + /** + * Returns if a bingo is possible + * @returns {Promise} + */ + async bingo() { + let subMatrix = await this._getSubmittedMatrix(); + return checkBingo(subMatrix); + } + + /** + * Returns the lobby of the grid + * @returns {Promise} + */ + async lobby() { + await this._loadGridInfo(); + return new LobbyWrapper(this.lobbyId); + } + + /** + * Returns the player of the grid + * @returns {Promise} + */ + async player() { + await this._loadGridInfo(); + return new PlayerWrapper(this.playerId); + } + + /** + * Toggles submitted of a grid field + * @param row {Number} - the row of the field + * @param column {Number} - the column of the field + * @returns {Promise} + */ + async toggleField(row, column) { + let result = await bdm.toggleGridFieldSubmitted(this.id, row, column); + return new GridFieldWrapper(result); } } -class BingoUser { +class MessageWrapper { /** - * Bingo User class to store user information + * constructor + * @param row {Object} - the database row of the message */ - constructor() { - this.id = generateBingoId(); - this.game = null; - this.username = 'anonymous'; - this.grids = {}; + constructor(row) { + this.id = row.id; + this.content = row.content; + this.htmlContent = md.renderInline(this.content); + this.author = new PlayerWrapper(row.player_id); + this.lobby = new LobbyWrapper(row.lobby_id); + this.type = row.type; + this.created = row.created; } } -class BingoWordField { +class PlayerWrapper { + /** + * constructor + * @param id {Number} - the id of the player + */ + constructor(id) { + this.id = id; + this._infoLoaded = false; + } + /** - * Represents a single bingo field with the word an the status. - * It also holds the base64-encoded word. - * @param word + * Loads all player information + * @returns {Promise} + * @private */ - constructor(word) { - this.word = word; - this.base64Word = Buffer.from(word).toString('base64'); - this.submitted = false; + async _loadPlayerInfo() { + if (!this._infoLoaded) { + let result = await bdm.getPlayerInfo(this.id); + if (result) { + this._uname = result.username; + this.expire = result.expire; + this._infoLoaded = true; + } + } + } + + /** + * Returns the grid for a specific lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise} + */ + async grid({lobbyId}) { + let currentRound = (await new LobbyWrapper(lobbyId).currentRound()).id; + let result = await bdm.getGridForPlayerLobbyRound(lobbyId, this.id, currentRound); + if (result) + return new GridWrapper(result.id); + } + + /** + * Returns the username of the player + * @returns {Promise} + */ + async username() { + await this._loadPlayerInfo(); + return this._uname; + } + + /** + * Returns the number of wins of a player in a lobby + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise} + */ + async wins({lobbyId}) { + let result = await bdm.getLobbyPlayerWins(lobbyId, this.id); + if (result && result.wins) + return result.wins; + else + return null; } } -class BingoGrid { +class RoundWrapper { + /** + * constructor + * @param id {Number} - the id of the round + * @param [row] {Object} - already queried row of the row + */ + constructor(id, row) { + this.id = id; + this._infoLoaded = false; + if (row) + this._assignProperties(row); + } + + /** + * Adds data to the round wrapper + * @param row + * @private + */ + _assignProperties(row) { + this._start = row.start; + this._finish = row.finish; + this._status = row.status; + this._lobbyId = row.lobby_id; + this._winnerId = row.winner; + this._infoLoaded = true; + } + + /** + * Loads the round info from the database + * @returns {Promise} + * @private + */ + async _loadRoundInfo() { + if (!this._infoLoaded) { + let row = await bdm.getRoundInfo(this.id); + if (row) + this._assignProperties(row); + } + } + + /** + * Returns the winner of the bingo round (if exists) + * @returns {Promise} + */ + async winner() { + await this._loadRoundInfo(); + if (this._winnerId) + return new PlayerWrapper(this._winnerId); + } + + /** + * Returns the start timestamp of the round + * @returns {Promise} + */ + async start() { + await this._loadRoundInfo(); + return this._start; + } + + /** + * Returns the finish timestamp of the round if it exists + * @returns {Promise} + */ + async finish() { + await this._loadRoundInfo(); + return this._finish; + } + + /** + * Returns the status of a round + * @returns {Promise} + */ + async status() { + await this._loadRoundInfo(); + return this._status; + } + + /** + * Returns the lobby the round is belonging to + * @returns {Promise} + */ + async lobby() { + await this._loadRoundInfo(); + return new LobbyWrapper(this._lobbyId); + } + + /** + * Updates the status of the round to a new one + * @param status {String<8>} - the new status + * @returns {Promise} + */ + async updateStatus(status) { + let updateResult = await bdm.updateRoundStatus(this.id, status); + if (updateResult) + this._assignProperties(updateResult); + } + + /** + * Sets the round to finished + * @returns {Promise} + */ + async setFinished() { + let updateResult = await bdm.setRoundFinished(this.id); + if (updateResult) + this._assignProperties(updateResult); + } + + /** + * Sets the winner of the round + * @param winnerId {Number} - the id of the winner + */ + async setWinner(winnerId) { + let status = await this.status(); + if (status !== "FINISHED") { + let updateResult = await bdm.setRoundWinner(winnerId); + if (updateResult) + await this.setFinished(); + return true; + } + } +} + +class LobbyWrapper { + /** + * constructor + * @param id {Number} - the id of the lobby + * @param [row] {Object} - the optional row object of the lobby to load info from + */ + constructor(id, row) { + this.id = id; + this._infoLoaded = false; + if (row) + this._assignProperties(row); + } + + /** + * Loads information about the lobby if it hasn't been loaded yet + * @returns {Promise} + * @private + */ + async _loadLobbyInfo() { + if (!this._infoLoaded) { + let row = await bdm.getLobbyInfo(this.id); + this._assignProperties(row); + } + } + + /** + * Assigns properties to the lobby wrapper + * @param row {Object} - the row to assign properties from + * @private + */ + _assignProperties(row) { + if (row) { + this.admin_id = row.admin_id; + this.grid_size = row.grid_size; + this.expire = row.expire; + this.current_round = row.current_round; + this._infoLoaded = true; + } + } + + /** + * returns the players in the lobby + * @returns {Promise} + */ + async players() { + let rows = await bdm.getLobbyPlayers(this.id); + let players = []; + for (let row of rows) + players.push(new PlayerWrapper(row.player_id)); + return players; + } + + /** + * Returns the admin of the lobby + * @returns {Promise} + */ + async admin() { + await this._loadLobbyInfo(); + return new PlayerWrapper(this.admin_id); + } + + /** + * Returns the active round of the lobby + * @returns {Promise} + */ + async currentRound() { + await this._loadLobbyInfo(); + if (this.current_round) + return new RoundWrapper(this.current_round); + } + + /** + * Returns all round of a lobby + * @returns {Promise} + */ + async rounds() { + let rows = await bdm.getLobbyRounds(this.id); + let rounds = []; + for (let row of rows) + rounds.push(new RoundWrapper(row.id, row)); + return rounds; + } + + /** + * Returns the grid-size of the lobby + * @returns {Promise} + */ + async gridSize() { + await this._loadLobbyInfo(); + return this.grid_size; + } + + /** + * Returns a number of messages send in the lobby + * @param limit + * @returns {Promise} + */ + async messages({limit}) { + let rows = await bdm.getLobbyMessages(this.id, limit); + let messages = []; + for (let row of rows) + messages.push(new MessageWrapper(row)); + return messages; + } + + /** + * Returns all words in a lobby + * @returns {Promise} + */ + async words() { + let rows = await bdm.getWordsForLobby(this.id); + let words = []; + for (let row of rows) + words.push(new WordWrapper(row.id, row)); + return words; + } + + /** + * Creates a new round + * @returns {Promise<*>} + */ + async _createRound() { + let result = await bdm.addRound(this.id); + if (result && result.id) { + let updateResult = await bdm.setLobbyCurrentRound(this.id, result.id); + this._assignProperties(updateResult); + return result.id; + } + } + + /** + * Creates a grid for each player + * @returns {Promise} + */ + async _createGrids() { + let words = await this.words(); + let players = await this.players(); + let currentRound = this.current_round; + for (let player of players) { + // eslint-disable-next-line no-await-in-loop + let gridId = (await bdm.addGrid(this.id, player.id, currentRound)).id; + let gridContent = generateWordGrid(this.grid_size, words); + + for (let i = 0; i < gridContent.length; i++) + for (let j = 0; j < gridContent[i].length; j++) + // eslint-disable-next-line no-await-in-loop + await bdm.addWordToGrid(gridId, gridContent[i][j].id, i, j); + } + } + + /** + * Creates a new round and new grids for each player + * @returns {Promise} + */ + async startNewRound() { + let words = await this.words(); + if (words && words.length > 0) { + let currentRound = await this.currentRound(); + if (currentRound) + await currentRound.setFinished(); + await this._createRound(); + await this._createGrids(); + } + } + + /** + * Sets the grid size of the lobby + * @param gridSize {Number} - the new grid size + * @returns {Promise} + */ + async setGridSize(gridSize) { + let updateResult = await bdm.setLobbyGridSize(this.id, gridSize); + this._assignProperties(updateResult); + } + + /** + * Returns if the specific player exists in the lobby + * @param playerId + * @returns {Promise<*>} + */ + async hasPlayer(playerId) { + let result = (await bdm.getPlayerInLobby(playerId, this.id)); + return (result && result.player_id); + } + /** - * Represents the bingo grid containing all the words. - * @param wordGrid - * @returns {BingoGrid} + * Sets the words of the lobby + * @param words + * @returns {Promise} */ - constructor(wordGrid) { - this.wordGrid = wordGrid; - this.fieldGrid = wordGrid.map(x => x.map(y => new BingoWordField(y))); - this.bingo = false; - return this; + async setWords(words) { + if (words.length > 0) { + await bdm.clearLobbyWords(this.id); + for (let word of words) + // eslint-disable-next-line no-await-in-loop + await bdm.addWordToLobby(this.id, word); + } } } + /** * Replaces tag signs with html-escaped signs. * @param htmlString @@ -406,45 +1042,21 @@ function inflateArray(array, minSize) { return resultArray; } -/** - * Generates an id for a subreddit download. - * @returns {string} - */ -function generateBingoId() { - return Date.now().toString(16); -} - /** * Generates a word grid with random word placements in the given dimensions - * @param dimensions {Array} - the dimensions of the grid - * @param words {Array} - the words included in the grid - * @returns {BingoGrid} + * @param size {Array} - the dimensions of the grid + * @param words {Array<*>} - the words included in the grid + * @returns {Array} */ -function generateWordGrid(dimensions, words) { - let shuffledWords = shuffleArray(inflateArray(words, dimensions[0]*dimensions[1])); +function generateWordGrid(size, words) { + let shuffledWords = shuffleArray(inflateArray(words, size**2)); let grid = []; - for (let x = 0; x < dimensions[1]; x++) { + for (let x = 0; x < size; x++) { grid[x] = []; - for (let y = 0; y < dimensions[0]; y++) - grid[x][y] = shuffledWords[(x * dimensions[0]) + y]; - + for (let y = 0; y < size; y++) + grid[x][y] = shuffledWords[(x * size) + y]; } - return (new BingoGrid(grid)); -} - -/** - * Sets the submitted parameter of the words in the bingo grid that match to true. - * @param base64Word {String} - base64 encoded bingo word - * @param bingoGrid {BingoGrid} - the grid where the words are stored - * @returns {boolean} - */ -function toggleHeared(base64Word, bingoGrid) { - for (let row of bingoGrid.fieldGrid) - for (let field of row) - if (base64Word === field.base64Word) - field.submitted = !field.submitted; - checkBingo(bingoGrid); - return true; + return grid; } /** @@ -502,30 +1114,14 @@ function checkBingoHorizontal(fg) { /** * Checks if a bingo exists in the bingo grid. - * @param bingoGrid {BingoGrid} + * @param fg {Array<[Boolean]>} * @returns {boolean} */ -function checkBingo(bingoGrid) { - let fg = bingoGrid.fieldGrid.map(x => x.map(y => y.submitted)); +function checkBingo(fg) { let diagonalBingo = checkBingoDiagnoal(fg); - if (diagonalBingo) { - bingoGrid.bingo = true; - return true; - } let verticalCheck = checkBingoVertical(fg); - - if (verticalCheck) { - bingoGrid.bingo = true; - return true; - } let horizontalCheck = checkBingoHorizontal(fg); - - if (horizontalCheck) { - bingoGrid.bingo = true; - return true; - } - bingoGrid.bingo = false; - return false; + return diagonalBingo || verticalCheck || horizontalCheck; } @@ -539,8 +1135,6 @@ router.init = async () => { }; router.use(async (req, res, next) => { - if (!req.session.bingoUser) - req.session.bingoUser = new BingoUser(); if (req.session.bingoPlayerId) await bdm.updatePlayerExpiration(req.session.bingoPlayerId); next(); @@ -558,7 +1152,7 @@ router.get('/', (req, res) => { bingoSession.addUser(bingoUser); if (!bingoUser.grids[gameId]) - bingoUser.grids[gameId] = generateWordGrid([bingoSession.gridSize, bingoSession.gridSize], bingoSession.words); + bingoUser.grids[gameId] = generateWordGrid(bingoSession.gridSize, bingoSession.words); res.render('bingo/bingo-game', { grid: bingoUser.grids[gameId].fieldGrid, @@ -574,129 +1168,113 @@ router.get('/', (req, res) => { }); router.graphqlResolver = async (req, res) => { - if (req.session.bingoPlayerId) - await bdm.updatePlayerExpiration(req.session.bingoPlayerId); - let bingoUser = req.session.bingoUser || new BingoUser(); - let gameId = req.query.game || bingoUser.game || null; - let bingoSession = bingoSessions[gameId]; + let playerId = req.session.bingoPlayerId; + if (playerId) + await bdm.updatePlayerExpiration(playerId); return { // queries - gameInfo: ({input}) => { - if (input && input.id) - return bingoSessions[input.id]; - else - return bingoSession; - }, - checkBingo: () => { - return checkBingo(bingoUser.grids[gameId]); + lobby: ({id}) => { + return new LobbyWrapper(id); }, - activeGrid: () => { - return bingoUser.grids[gameId]; - }, - // mutation - createLobby: async ({input}) => { - let gridSize = (input && input.gridSize)? input.gridSize : 3; - let lobby = await bdm.createLobby(req.session.bingoPlayerId, gridSize); - if (lobby && lobby.id) - return lobby.id; + player: ({id}) => { + if (id) + return new PlayerWrapper(id); else - res.status(500); - }, - joinLobby: async ({input}) => { - if (input.lobbyId) { - let entry = await bdm.addPlayerToLobby(req.session.bingoPlayerId, input.lobbyId); - if (entry && entry.lobby_id && entry.player_id) - return { - lobbyId: entry.lobby_id, - playerId: entry.player_id - }; + if (playerId) + return new PlayerWrapper(playerId); else - res.status(500); - } else { - res.status(400); - } + res.status(400); }, - createGame: ({input}) => { - let words = input.words.filter((el) => { // remove empty strings and non-types from word array - return (!!el && el.length > 0); - }); - let size = input.size; - if (words.length > 0 && size < 10 && size > 0) { - words = words.slice(0, 10000); // only allow up to 10000 words in the bingo - let game = new BingoSession(words, size); - - bingoSessions[game.id] = game; + // mutations + setUsername: async ({username}) => { + username = username.substring(0, 30); // only allow 30 characters - 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])) { - 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]; - }, 300000); - return bingoSession; + if (!playerId) { + req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; + playerId = req.session.bingoPlayerId; } else { - return bingoSession; + await bdm.updatePlayerUsername(playerId, username); } + return new PlayerWrapper(playerId); }, - toggleWord: ({input}) => { - if (input.word || input.base64Word) { - input.base64Word = input.base64Word || Buffer.from(input.word).toString('base-64'); - if (bingoUser.grids[gameId]) { - toggleHeared(input.base64Word, bingoUser.grids[gameId]); - bingoSession.sendToggleInfo(input.base64Word, bingoUser); - return bingoUser.grids[gameId]; - } else { - res.status(400); - } + createLobby: async({gridSize}) => { + if (playerId) { + let result = await bdm.createLobby(playerId, gridSize); + return new LobbyWrapper(result.id); } else { res.status(400); } }, - setUsername: async ({input}) => { - if (input.username) { - bingoUser.username = input.username.substring(0, 30); // only allow 30 characters - - if (!req.session.bingoPlayerId) - req.session.bingoPlayerId = (await bdm.addPlayer(input.username)).id; - else - await bdm.updatePlayerUsername(req.session.bingoPlayerId, input.username); - 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); - - }, - sendChatMessage: ({input}) => { - input.message = replaceTagSigns(input.message).substring(0, 250); - if (bingoSession && input.message) { - let userMessage = new BingoChatMessage(input.message, 'USER', bingoUser.username); - bingoSession.chatMessages.push(userMessage); - return userMessage; - } else { - res.status(400); - } + mutateLobby: async ({id}) => { + let lobbyId = id; + await bdm.updateLobbyExpiration(lobbyId); + let lobbyWrapper = new LobbyWrapper(lobbyId); + return { + join: async () => { + if (playerId) { + let result = await bdm.addPlayerToLobby(playerId, lobbyId); + return new LobbyWrapper(result.lobby_id); + } else { + res.status(400); + } + }, + leave: async () => { + if (playerId) { + await bdm.removePlayerFromLobby(playerId, lobbyId); + return true; + } else { + res.status(400); + } + }, + kickPlayer: async ({pid}) => { + let result = await bdm.removePlayerFromLobby(pid, lobbyId); + return new LobbyWrapper(result.id, result); + }, + startRound: async () => { + await lobbyWrapper.startNewRound(); + return lobbyWrapper.currentRound(); + }, + setGridSize: async ({gridSize}) => { + await lobbyWrapper.setGridSize(gridSize); + return lobbyWrapper; + }, + setWords: async({words}) => { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.setWords(words); + return lobbyWrapper; + } else { + res.status(400); + } + }, + sendMessage: async ({message}) => { + if (await lobbyWrapper.hasPlayer(playerId)) { + let result = await bdm.addUserMessage(lobbyId, playerId, message); + return new MessageWrapper(result); + } else { + res.status(400); + } + }, + submitBingo: async () => { + let isBingo = await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})).bingo(); + let currentRound = await lobbyWrapper.currentRound(); + if (isBingo) { + let result = await currentRound.setWinner(playerId); + if (result) + return currentRound; + else + res.status(400); + } else { + res.status(400); + } + }, + toggleGridField: async ({location}) => { + let {row, column} = location; + return await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})) + .toggleField(row, column); + } + }; } }; }; diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql index dd3c193..b14176d 100644 --- a/sql/bingo/createBingoTables.sql +++ b/sql/bingo/createBingoTables.sql @@ -9,7 +9,8 @@ CREATE TABLE IF NOT EXISTS bingo.players ( CREATE TABLE IF NOT EXISTS bingo.lobbys ( id serial UNIQUE PRIMARY KEY, admin_id serial references bingo.players(id) ON DELETE SET NULL, - grid_size integer DEFAULT 3, + grid_size integer DEFAULT 3 NOT NULL, + current_round integer, expire timestamp DEFAULT (NOW() + interval '1 hour' ) ); @@ -25,33 +26,17 @@ CREATE TABLE IF NOT EXISTS bingo.lobby_players ( CREATE TABLE IF NOT EXISTS bingo.words ( id serial UNIQUE PRIMARY KEY, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, - heared integer DEFAULT 0, + heared integer DEFAULT 0 NOT NULL, content varchar(254) NOT NULL ); --- grids table -CREATE TABLE IF NOT EXISTS bingo.grids ( - id serial UNIQUE PRIMARY KEY, - player_id serial references bingo.players(id) ON DELETE CASCADE, - lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE -); - --- grids_words table -CREATE TABLE IF NOT EXISTS bingo.grid_words ( - grid_id serial references bingo.grids(id) ON DELETE CASCADE, - word_id serial references bingo.words(id) ON DELETE CASCADE, - grid_row integer NOT NULL, - grid_column integer NOT NULL, - PRIMARY KEY (grid_id, word_id) -); - -- messages table CREATE TABLE IF NOT EXISTS bingo.messages ( id serial UNIQUE PRIMARY KEY, content varchar(255) NOT NULL, player_id serial references bingo.players(id) ON DELETE SET NULL, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, - type varchar(8), + type varchar(8) DEFAULT 'USER' NOT NULL, created timestamp DEFAULT NOW() ); @@ -60,10 +45,26 @@ CREATE TABLE IF NOT EXISTS bingo.rounds ( id serial UNIQUE PRIMARY KEY, start timestamp DEFAULT NOW(), finish timestamp, - status varchar(8) DEFAULT 'undefined', + status varchar(8) DEFAULT 'ACTIVE', lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, - winner serial references bingo.players(id) ON DELETE SET NULL + winner integer ); --- altering -ALTER TABLE bingo.lobbys ADD COLUMN IF NOT EXISTS current_round serial references bingo.rounds(id) ON DELETE SET NULL; +-- grids table +CREATE TABLE IF NOT EXISTS bingo.grids ( + id serial UNIQUE PRIMARY KEY, + player_id serial references bingo.players(id) ON DELETE CASCADE, + lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, + round_id serial references bingo.rounds(id) ON DELETE CASCADE, + UNIQUE(player_id, lobby_id, round_id) +); + +-- grids_words table +CREATE TABLE IF NOT EXISTS bingo.grid_words ( + grid_id serial references bingo.grids(id) ON DELETE CASCADE, + word_id serial references bingo.words(id) ON DELETE CASCADE, + grid_row integer NOT NULL, + grid_column integer NOT NULL, + submitted boolean DEFAULT false, + PRIMARY KEY (grid_id, grid_row, grid_column) +); diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index d8e3efa..bb710f3 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -23,13 +23,13 @@ addPlayer: # - {String} - the new username of the player # - {Number} - the id of the player updatePlayerUsername: - sql: UPDATE bingo.players SET players.username = $1 WHERE players.id = $2 RETURNING *; + sql: UPDATE bingo.players SET username = $1 WHERE id = $2 RETURNING *; -# Selects the username for a player id +# Selects the row for a player id # params: # - {Number} - the id of the player -getPlayerUsername: - sql: SELECT players.username FROM bingo.players WHERE id = $1; +getPlayerInfo: + sql: SELECT * FROM bingo.players WHERE id = $1; # updates the expiration timestamp of the player # params: @@ -55,14 +55,14 @@ updateLobbyExpire: # - {Number} - the id of the player # - {Number} - the id of the lobby addPlayerToLobby: - sql: INSERT INTO bingo.lobby_players (player_id, lobby_id) VALUES ($1, $2); + sql: INSERT INTO bingo.lobby_players (player_id, lobby_id) VALUES ($1, $2) RETURNING *; # removes a player from a lobby # params: # - {Number} - the id of the player # - {Number} - the id of the lobby removePlayerFromLobby: - sql: REMOVE FROM bingo.lobby_players WHERE player_id = $1 AND lobby_id = $2; + sql: DELETE FROM bingo.lobby_players WHERE player_id = $1 AND lobby_id = $2; # returns the entry of the player and lobby # params: @@ -71,6 +71,82 @@ removePlayerFromLobby: getPlayerInLobby: sql: SELECT * FROM bingo.lobby_players lp WHERE lp.player_id = $1 AND lp.lobby_id = $2; +# returns all players in a lobby +# params: +# - {Number} - the id of the lobby +getLobbyPlayers: + sql: SELECT * FROM bingo.lobby_players WHERE lobby_players.lobby_id = $1; + +# returns all direct information about the lobby +# params: +# - {Number} - the id of the lobby +getLobbyInfo: + sql: SELECT * FROM bingo.lobbys WHERE lobbys.id = $1; + +# returns the last $2 messages for a lobby +# params: +# - {Number} - the id of the lobby +# - {Number} - the limit of messages to fetch +getLobbyMessages: + sql: SELECT * FROM bingo.messages WHERE messages.lobby_id = $1 ORDER BY messages.created DESC LIMIT $2; + +# returns the number of wins a user had in a lobby +# - {Number} - the id of the lobby +# - {Number} - the id of the player +getLobbyPlayerWins: + sql: SELECT COUNT(*) wins FROM bingo.rounds WHERE rounds.lobby_id = $1 AND rounds.winner = $2; + +# returns all rounds of a lobby +# params: +# - {Number} - the id of the lobby +getLobbyRounds: + sql: SELECT * FROM bingo.rounds WHERE rounds.lobby_id = $1; + +# creates a round entry +# params: +# - {Number} - the id of the lobby +addRound: + sql: INSERT INTO bingo.rounds (lobby_id) VALUES ($1) RETURNING *; + +# updates the status of a round +# params: +# - {Number} - the id of the round +# - {Number} - the new status +updateRoundStatus: + sql: UPDATE bingo.rounds SET status = $2 WHERE id = $1 RETURNING *; + +# updates the status of the round to finished +# params: +# - {Number} - the id of the round +setRoundFinished: + sql: UPDATE bingo.rounds SET status = 'FINISHED', finish = NOW() WHERE id = $1 RETURNING *; + +# updates the winner of the round +# params: +# - {Number} - the id of the winner +setRoundWinner: + sql: UPDATE bingo.rounds SET winner = $2 WHERE id = $1 RETURNING *; + +# sets the current round of a lobby +# params: +# - {Number} - the id of the lobby +# - {Number} - the id of the round +setLobbyCurrentRound: + sql: UPDATE bingo.lobbys SET current_round = $2 WHERE id = $1 RETURNING *; + +# sets the grid size of a lobby +# params: +# - {Number} - the id of the lobby +# - {Number} - the new grid size +setLobbyGridSize: + sql: UPDATE bingo.lobbys SET grid_size = $2 WHERE id = $1 RETURNING *; + +# returns information about a round +# params: +# - {Number} - the id of the round +getRoundInfo: + sql: SELECT * FROM bingo.rounds WHERE rounds.id = $1; + # adds a word to the database # params: # - {Number} - the id of the lobby where the word is used @@ -78,18 +154,47 @@ getPlayerInLobby: addWord: sql: INSERT INTO bingo.words (lobby_id, content) VALUES ($1, $2) RETURNING *; +# deletes all words of a lobby +# params: +# - {Number} - the id of the lobby +clearLobbyWords: + sql: DELETE FROM bingo.words WHERE lobby_id = $1; + # returns all words for a bingo game (lobby) # params: # - {Number} - the id of the bingo lobby getWordsForLobby: sql: SELECT * FROM bingo.words WHERE words.lobby_id = $1; +# returns the word row for an id +# params: +# - {Number} - the id of the word +getWordInfo: + sql: SELECT * FROM bingo.words WHERE words.id = $1; + # adds a grid to the database # params: # - {Number} - the id of the player # - {Number} - the id of the lobby +# - {Number} - the id of the round addGrid: - sql: INSERT INTO bingo.grids (player_id, lobby_id) VALUES ($1, $2) RETURNING *; + sql: INSERT INTO bingo.grids (player_id, lobby_id, round_id) VALUES ($1, $2, $3) RETURNING *; + +# returns the grid entry for a player and lobby id +# params: +# - {Number} - the id of the player +# - {Number} - the id of the lobby +# - {Number} - the id of the round +getGridByPlayerLobbyRound: + sql: SELECT * FROM bingo.grids WHERE player_id = $1 AND lobby_id = $2 AND round_id = $3; + +# returns the grid row +# params: +# - {Number} - the id of the grid +getGridInfo: + sql: > + SELECT grids.id, grids.player_id, grids.lobby_id, lobbys.grid_size FROM bingo.grids, bingo.lobbys + WHERE grids.id = $1 AND grids.lobby_id = lobbys.id; # inserts grid-word connections into the database # params: @@ -100,8 +205,37 @@ addGrid: addWordToGrid: sql: INSERT INTO bingo.grid_words (grid_id, word_id, grid_row, grid_column) VALUES ($1, $2, $3, $4) RETURNING *; +# sets a bingo field to submitted = not submitted +# params: +# - {Number} - the id of the grid +# - {Number} - the row of the field +# - {Number} - the column of the field +toggleGridFieldSubmitted: + sql: > + UPDATE bingo.grid_words gw SET submitted = not submitted + WHERE gw.grid_id = $1 + AND gw.grid_row = $2 + AND gw.grid_column = $3 + RETURNING *; + +# selects a single field from grid_words +# params: +# - {Number} - the id of the grid +# - {Number} - the row of the field +# - {Number} - the column of the field +getGriedField: + sql: SELECT * FROM bingo.grid_words WHERE grid_words.grid_id = $1 AND grid_words.row = $2 and grid_words.column = $3; + # returns all words for a players grid # params: # - {Number} - the id of the grid getWordsForGridId: sql: SELECT * FROM bingo.grid_words, bingo.words WHERE grid_words.grid_id = $1 AND words.id = grid_words.word_id; + +# inserts a user message +# params: +# - {Number} - the id of the user +# - {Number} - the id of the lobby +# - {String} - the content of the message +addUserMessage: + sql: INSERT INTO bingo.messages (player_id, lobby_id, content) VALUES ($1, $2, $3) RETURNING *; From 5c10862b196e64bf890e802917ce6f4eec817bf6 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Tue, 14 May 2019 21:33:05 +0200 Subject: [PATCH 28/42] Added bingo startpage (wip) - added css for startpage (wip) - added file for css animations - added pug file for bingo starpage --- CHANGELOG.md | 5 + public/javascripts/bingo-web.js | 331 ++--------------------- public/javascripts/common.js | 31 +++ public/stylesheets/sass/animations.sass | 15 + public/stylesheets/sass/bingo/style.sass | 57 ++-- public/stylesheets/sass/classes.sass | 26 ++ public/stylesheets/sass/mixins.sass | 6 + public/stylesheets/sass/vars.sass | 2 + routes/bingo.js | 29 +- views/bingo/bingo-create.pug | 16 ++ 10 files changed, 170 insertions(+), 348 deletions(-) create mode 100644 public/stylesheets/sass/animations.sass create mode 100644 views/bingo/bingo-create.pug diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e50695..f76a312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,15 +20,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - sql scripts for bingo - data management class for bingo - libs with utils and global variables +- css for startpage (wip) +- file for css animations +- pug file for startpage ## Changed - changed export of `app.js` to the asynchronous init function that returns the app object - `bin/www` now calls the init function of `app.js` +- graphql api ### Removed - sqlite3 sesssion storage +- old frontend ### Fixed diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 0b953c6..7babd77 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -5,272 +5,41 @@ */ function getGameParam() { let matches = window.location.href.match(/\?game=(\w+)/); - if (matches) + if (matches) { return matches[1]; - else + } else { return ''; + } } /** - * Submits the bingo words to create a game - * @returns {Promise} + * Submits the value of the username-input to set the username. + * @returns {Promise} */ -async function submitBingoWords() { - let textContent = document.querySelector('#bingo-textarea').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; +async function submitUsername() { + let unameInput = document.querySelector('#input-username'); + let username = unameInput.value.replace(/^\s+|\s+$/g, ''); + if (username.length > 1) { let response = await postGraphqlQuery(` - mutation($words:[String!]!, $size:Int!) { + mutation($username:String!) { bingo { - createGame(input: { - words: $words, - size: $size - }) { + setUsername(username: $username) { id + username } } - }`, { - words: words, - size: Number(size) - }, `/graphql?game=${getGameParam()}`); + }`, {username: username}); 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} - */ -async function createFollowup() { - let response = await postGraphqlQuery(` - mutation { - bingo { - createFollowupGame { - id - } - } - }`, null, `/graphql?game=${getGameParam()}`); - if (response.status === 200 && response.data.bingo.createFollowupGame) { - let gameid = response.data.bingo.createFollowupGame.id; - insertParam('game', gameid); - } else { - showError(`Failed to create follow up game. HTTP Error: ${response.status}`); - console.error(response); - } -} - -/** - * Submits the value of the username-input to set the username. - * @returns {Promise} - */ -async function submitUsername() { - let unameInput = document.querySelector('#username-input'); - let username = unameInput.value.replace(/^\s+|\s+$/g, ''); - if (username.length > 1 && username !== 'anonymous') { - let response = await postGraphqlQuery(` - mutation($username:String!) { - bingo { - setUsername(input: {username: $username}) { - id - username - } - } - }`, { - username: username - }, `/graphql?game=${getGameParam()}`); - if (response.status === 200) { - unameInput.value = ''; - unameInput.placeholder = response.data.username; - document.querySelector('#username-form').remove(); - document.querySelector('.greyover').remove(); + return true; } else { showError(`Failed to submit username. HTTP Error: ${response.status}`); console.error(response); + return false; } } else { showError('You need to provide a username (minimum 2 characters)!'); - } -} - -/** - * toggles a word (toggle occures on response) - * @param word {String} - the base64 encoded bingo word - * @returns {Promise} - */ -async function submitWord(word) { - let response = await postGraphqlQuery(` - mutation($word:String!) { - bingo { - toggleWord(input: {base64Word: $word}) { - bingo - fieldGrid { - submitted - base64Word - } - } - } - }`, { - word: word - }, `/graphql?game=${getGameParam()}`); - - 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'); - - } else { - showError(`Failed to submit word. HTTP Error: ${response.status}`); - console.error(response); - } -} - -/** - * Refreshes the bingo grid. Shows the bingo button if a bingo is possible - * @returns {Promise} - */ -async function refreshBingoGrid() { - let response = await postGraphqlQuery(` - query { - bingo { - activeGrid { - bingo - fieldGrid { - word - base64Word - submitted - } - } - } - }`, {}, `/graphql?game=${getGameParam()}`); - - if (response.status === 200 && response.data.bingo.activeGrid) { - let fieldGrid = response.data.bingo.activeGrid.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.activeGrid.bingo) - document.querySelector('#bingo-button').setAttribute('class', ''); - else - document.querySelector('#bingo-button').setAttribute('class', 'hidden'); - - } else { - showError(`Failed to submit word. HTTP Error: ${response.status}`); - console.error(response); - } -} - -/** - * Submits a bingo (Bingo button is pressed). - * The game is won if the backend validated it. - * @returns {Promise} - */ -async function submitBingo() { - 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 { - showError(`Failed to submit Bingo. HTTP Error: ${response.status}`); - console.error(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} - */ -async function refresh() { - await refreshBingoGrid(); - let response = await postGraphqlQuery(` - query { - bingo { - gameInfo { - id - bingos - players { - username - id - } - getMessages { - id - username - type - htmlContent - } - } - } - }`, 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 = `${player.username}`; - document.querySelector('#players-container').appendChild(playerDiv); - } else { - let playerNameSpan = foundPlayerDiv.querySelector('.player-name-span'); - if (playerNameSpan.innerText !== player.username) - playerNameSpan.innerText = player.username; - - } - } - } - for (let chatMessage of bingoSession.getMessages) - if (!document.querySelector(`.chatMessage[msg-id='${chatMessage.id}'`)) - addChatMessage(chatMessage); - - } else { - if (response.status === 400) - clearInterval(refrInterval); - console.error(response); - showError('No session found. Are cookies allowed?'); + return false; } } @@ -304,40 +73,16 @@ function showError(errorMessage) { errorDiv.setAttribute('class', 'errorDiv'); errorDiv.innerHTML = `${errorMessage}`; let contCont = document.querySelector('#content-container'); - if (contCont) + if (contCont) { contCont.appendChild(errorDiv); - else + } else { alert(errorMessage); + } setTimeout(() => { errorDiv.remove(); }, 10000); } -async function sendChatMessage() { - let messageInput = document.querySelector('#chat-input'); - if (messageInput.value && messageInput.value.length > 0) { - let message = messageInput.value; - let response = await postGraphqlQuery(` - mutation($message: String!) { - bingo { - sendChatMessage(input: { message: $message }) { - id - htmlContent - username - type - } - } - }`, {message: message}, `/graphql?game=${getGameParam()}`); - if (response.status === 200) { - addChatMessage(response.data.bingo.sendChatMessage); - messageInput.value = ''; - } else { - console.error(response); - showError('Error when sending message.'); - } - } -} - /** * Adds a message to the chat * @param messageObject {Object} - the message object returned by graphql @@ -346,55 +91,25 @@ function addChatMessage(messageObject) { let msgSpan = document.createElement('span'); msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('msg-id', messageObject.id); - if (messageObject.type === "USER") + if (messageObject.type === "USER") { msgSpan.innerHTML = ` ${messageObject.username}: ${messageObject.htmlContent}`; - else + } else { msgSpan.innerHTML = ` ${messageObject.htmlContent}`; + } let chatContent = document.querySelector('#chat-content'); chatContent.appendChild(msgSpan); chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom } -/** - * 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(); -} - -/** - * Toggles the displayChat class on the content container to switch between chat-view and grid view - */ -function toggleChatView() { - let contentContainer = document.querySelector('#content-container'); - if (contentContainer.getAttribute('class') === 'displayChat') - contentContainer.setAttribute('class', ''); - else - contentContainer.setAttribute('class', 'displayChat'); -} -window.addEventListener("unhandledrejection", function(promiseRejectionEvent) { +window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { promiseRejectionEvent.promise.catch(err => console.log(err)); showError('Connection problems... Is the server down?'); }); window.onload = () => { - if (document.querySelector('#chat-container')) - refresh(); - if (window && !document.querySelector('#bingoform')) - refrInterval = setInterval(refresh, 1000); // eslint-disable-line no-undef - - let gridSizeElem = document.querySelector('#bingo-grid-size'); - 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:`; - }; }; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index b38cacd..91c1c13 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -91,3 +91,34 @@ function insertParam(key, value) { kvp[kvp.length] = [key, value].join('='); document.location.search = kvp.join('&'); } + +/** + * 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(); +} + +/** + * Wrapper around a function to use the status indicator + * @param func {function} - the function to execute + * @param indicatorSelector {String} - a selector for the status indicator + * @returns {Promise} + */ +async function indicateStatus(func, indicatorSelector) { + let statusIndicator = document.querySelector(indicatorSelector); + statusIndicator.setAttribute('status', 'pending'); + try { + let result = await func(); + if (result) + statusIndicator.setAttribute('status', 'success'); + else + statusIndicator.setAttribute('status', 'error'); + } catch (err) { + console.error(err); + statusIndicator.setAttribute('status', 'error'); + } +} diff --git a/public/stylesheets/sass/animations.sass b/public/stylesheets/sass/animations.sass new file mode 100644 index 0000000..69245b0 --- /dev/null +++ b/public/stylesheets/sass/animations.sass @@ -0,0 +1,15 @@ +@keyframes pulse-text + 0% + font-size: 0.8em + 50% + font-size: 1.2em + 100% + font-size: 0.8em + +@keyframes pulse-opacity + 0% + opacity: 0.8 + 50% + opacity: 1 + 100% + opacity: 0.8 diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 5a95460..9596e6a 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -41,10 +41,6 @@ textarea #words-container display: none !important - #username-form - width: calc(100% - 2rem) !important - left: 0 !important - #hide-player-container-button display: none @@ -138,32 +134,6 @@ textarea #bingo-button transition-duration: 0.8s -#username-form - @include default-element - position: fixed - display: block - height: calc(50% - 1rem) - width: calc(40% - 1rem) - top: 25% - left: 30% - text-align: center - vertical-align: middle - padding: 1rem - z-index: 1000 - - 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: grid grid-template-columns: 25% 75% @@ -299,3 +269,30 @@ textarea top: 0 left: 0 background-color: rgba(0, 0, 0, 0.5) + +#container-bingo-create + display: grid + grid-template-columns: 10% 80% 10% + grid-template-rows: 5% 10% 10% 70% 5% + height: 100% + width: 100% + + #username-form + @include gridPosition(2, 3, 2, 3) + margin: auto + + .statusIndicator + height: 1em + width: 1em + display: inline-block + margin: auto 1em + + #input-username + margin: 0 0 0 3em + + #lobby-form + @include gridPosition(3, 4, 2, 3) + margin: auto + + button + width: 100% diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index 2b4d91a..e73f69f 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -1,3 +1,6 @@ +@import vars +@import animations + .tableRow display: table-row @@ -17,3 +20,26 @@ .inline-grid display: inline-grid + +.statusIndicator + min-height: 1em + min-width: 1em + border-radius: 1em + transition-duration: 1s + +.statusIndicator:before + content: "" + +.statusIndicator[status='success']:before + content: "✓" + color: $success + +.statusIndicator[status='error']:before + content: "❌" + color: $error + +.statusIndicator[status='pending'] + background-color: $pending + animation-name: pulse-opacity + animation-duration: 5s + animation-iteration-count: infinite diff --git a/public/stylesheets/sass/mixins.sass b/public/stylesheets/sass/mixins.sass index 0b60140..17fdf5d 100644 --- a/public/stylesheets/sass/mixins.sass +++ b/public/stylesheets/sass/mixins.sass @@ -5,3 +5,9 @@ color: $primarySurface border: 2px solid $primarySurface transition-duration: 0.2s + +@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd) + grid-row-start: $rowStart + grid-row-end: $rowEnd + grid-column-start: $columnStart + grid-column-end: $columnEnd diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass index 365b26d..b88bf64 100644 --- a/public/stylesheets/sass/vars.sass +++ b/public/stylesheets/sass/vars.sass @@ -4,3 +4,5 @@ $secondary: teal $borderRadius: 20px $inactive: #aaa $error: #a00 +$success: #0a0 +$pending: #aa0 diff --git a/routes/bingo.js b/routes/bingo.js index 5e6785d..ea1b7f7 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1142,8 +1142,8 @@ router.use(async (req, res, next) => { router.get('/', (req, res) => { let bingoUser = req.session.bingoUser; - if (req.query.game) { - let gameId = req.query.game || bingoUser.game; + if (req.query.g) { + let lobbyId = req.query.g; if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { bingoUser.game = gameId; @@ -1163,7 +1163,7 @@ router.get('/', (req, res) => { res.render('bingo/bingo-submit'); } } else { - res.render('bingo/bingo-submit'); + res.render('bingo/bingo-create'); } }); @@ -1188,7 +1188,7 @@ router.graphqlResolver = async (req, res) => { }, // mutations setUsername: async ({username}) => { - username = username.substring(0, 30); // only allow 30 characters + username = replaceTagSigns(username.substring(0, 30)); // only allow 30 characters if (!playerId) { req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; @@ -1228,16 +1228,25 @@ router.graphqlResolver = async (req, res) => { } }, kickPlayer: async ({pid}) => { - let result = await bdm.removePlayerFromLobby(pid, lobbyId); - return new LobbyWrapper(result.id, result); + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + let result = await bdm.removePlayerFromLobby(pid, lobbyId); + return new LobbyWrapper(result.id, result); + } }, startRound: async () => { - await lobbyWrapper.startNewRound(); - return lobbyWrapper.currentRound(); + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.startNewRound(); + return lobbyWrapper.currentRound(); + } }, setGridSize: async ({gridSize}) => { - await lobbyWrapper.setGridSize(gridSize); - return lobbyWrapper; + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.setGridSize(gridSize); + return lobbyWrapper; + } }, setWords: async({words}) => { let admin = await lobbyWrapper.admin(); diff --git a/views/bingo/bingo-create.pug b/views/bingo/bingo-create.pug new file mode 100644 index 0000000..c767f52 --- /dev/null +++ b/views/bingo/bingo-create.pug @@ -0,0 +1,16 @@ +extends bingo-layout + +block content + div(id='container-bingo-create') + div(id='username-form') + input(id='input-username' + type='text' + placeholder='Enter your name' + onkeydown='submitOnEnter(event, () => indicateStatus(submitUsername, "#username-status"))') + button( + id='submit-username' + onclick='indicateStatus(submitUsername, "#username-status")') Set Username + div(id='username-status' class='statusIndicator') + div(id='lobby-form') + button(id='join-lobby' onclick='joinLobby()') Join Lobby + button(id='create-lobby' onclick='createLobby()') Create Lobby From 97861679432e79c45edf3e21533b2965c13e4f94 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Wed, 15 May 2019 23:46:01 +0200 Subject: [PATCH 29/42] Working frontend - tweaked backend - frontend is working again --- app.js | 2 +- graphql/bingo.graphql | 3 + lib/utils.js | 16 +- misc/usernames.txt | 11 + package.json | 2 +- public/javascripts/bingo-web.js | 508 ++++++++++++++++++++++- public/stylesheets/sass/animations.sass | 4 +- public/stylesheets/sass/bingo/style.sass | 377 +++++++---------- public/stylesheets/sass/classes.sass | 6 + public/stylesheets/sass/style.sass | 2 + routes/bingo.js | 153 +++++-- sql/bingo/createBingoTables.sql | 6 +- sql/bingo/queries.yaml | 7 + views/bingo/bingo-chat.pug | 4 + views/bingo/bingo-lobby.pug | 19 + views/bingo/bingo-players.pug | 6 + views/bingo/bingo-round.pug | 24 ++ views/bingo/bingo-submit.pug | 13 - 18 files changed, 866 insertions(+), 297 deletions(-) create mode 100644 misc/usernames.txt create mode 100644 views/bingo/bingo-chat.pug create mode 100644 views/bingo/bingo-lobby.pug create mode 100644 views/bingo/bingo-players.pug create mode 100644 views/bingo/bingo-round.pug delete mode 100644 views/bingo/bingo-submit.pug diff --git a/app.js b/app.js index 771587b..3f9a49c 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: true, + watchFiles: false, // TODO: set true logToConsole: true })); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index fa3d9df..9463835 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -139,6 +139,9 @@ type GridField { "the column of the field" column: Int! + + "the grid the field belongs to" + grid: BingoGrid } type BingoWord { diff --git a/lib/utils.js b/lib/utils.js index dbc8264..69e3835 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,10 @@ const yaml = require('js-yaml'), fsx = require('fs-extra'); +String.prototype.replaceAll = function(search, replacement) { + let target = this; + return target.replace(new RegExp(search, 'g'), replacement); +}; /** * Parses the `queries.yaml` file in the path. queries.yaml-format: @@ -33,7 +37,17 @@ function readSettings(path) { return settings; } +/** + * Returns all lines of a file as array + * @param fname {String} - the name of the file + * @returns {string[]} + */ +function getFileLines(fname) { + return fsx.readFileSync(fname).toString().replaceAll('\r\n', '\n').split('\n'); +} + Object.assign(exports, { parseSqlYaml: parseSqlYaml, - readSettings: readSettings + readSettings: readSettings, + getFileLines: getFileLines }); diff --git a/misc/usernames.txt b/misc/usernames.txt new file mode 100644 index 0000000..0ba10d3 --- /dev/null +++ b/misc/usernames.txt @@ -0,0 +1,11 @@ +Sharkinator +Dry River +Cool Dude +Noobmaster69 +TheLegend27 +BeastMaster64 +BitMaster +Angry Koala +Dragonslayer +Goblin Slayer +useless Aqua diff --git a/package.json b/package.json index 6b1c6db..48f6019 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "error", "last" ], - "no-await-in-loop": "warn", + "no-await-in-loop": "off", "curly": [ "warn", "multi", diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 7babd77..b9ad5fa 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -1,15 +1,27 @@ /* eslint-disable no-unused-vars, no-undef */ /** - * Returns the value of the url-param 'game' + * Returns the value of the url-param 'g' * @returns {string} */ -function getGameParam() { - let matches = window.location.href.match(/\?game=(\w+)/); - if (matches) { +function getLobbyParam() { + let matches = window.location.href.match(/\??&?g=(\d+)/); + if (matches) return matches[1]; - } else { + else + return ''; + +} + +/** + * REturns the value of the r url param + * @returns {string} + */ +function getRoundParam() { + let matches = window.location.href.match(/\??&?r=(\d+)/); + if (matches) + return matches[1]; + else return ''; - } } /** @@ -43,18 +55,228 @@ async function submitUsername() { } } +/** + * Creates a lobby and redirects to the lobby. + * @returns {Promise} + */ +async function createLobby() { + let response = await postGraphqlQuery(` + mutation { + bingo { + createLobby { + id + } + } + } + `); + if (response.status === 200 && response.data.bingo.createLobby) { + insertParam('g', response.data.bingo.createLobby.id); + return true; + } else { + showError('Failed to create Lobby. HTTP ERROR: ' + response.status); + console.error(response); + return false; + } +} + +/** + * Lets the player leave the lobby + * @returns {Promise} + */ +async function leaveLobby() { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + leave + } + } + } + `, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + insertParam('g', ''); + } else { + showError('Failed to leave lobby'); + console.error(response); + } +} + +/** + * Sends a message to the chat + * @returns {Promise} + */ +async function sendChatMessage() { + let messageInput = document.querySelector('#chat-input'); + if (messageInput.value && messageInput.value.length > 0) { + let message = messageInput.value; + messageInput.value = ''; + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!, $message:String!){ + bingo { + mutateLobby(id:$lobbyId) { + sendMessage(message:$message) { + id + htmlContent + type + author { + username + } + } + } + } + }`, {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.'); + } + } +} + +/** + * Sets the words for the lobby + * @param words + * @returns {Promise} + */ +async function setLobbyWords(words) { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!, $words:[String!]!){ + bingo { + mutateLobby(id:$lobbyId) { + setWords(words:$words) { + words { + content + } + } + } + } + }`, {lobbyId: getLobbyParam(), words: words}); + if (response.status === 200) { + return response.data.bingo.mutateLobby.setWords.words; + } else { + console.error(response); + showError('Error when setting lobby words.'); + } +} + +/** + * Starts a new round of bingo + * @returns {Promise} + */ +async function startRound() { + let words = getLobbyWords(); + let resultWords = await setLobbyWords(words); + textinput.value = resultWords.map(x => x.content).join('\n'); + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + startRound { + id + } + } + } + }`, {lobbyId: getLobbyParam()}); + + if (response.status === 200) { + insertParam('r', response.data.bingo.mutateLobby.startRound.id); + } else { + console.error(response); + showError('Error when starting round.'); + } +} + +/** + * Returns the words of the lobby word input. + * @returns {string[]} + */ +function getLobbyWords() { + let textinput = document.querySelector('#input-bingo-words'); + let words = textinput.value.replace(/[<>]/g, '').split('\n').filter((el) => { + return (!!el && el.length > 0); // remove empty strings and non-types from word array + }); + return words; +} + +/** + * Submits the toggle of a bingo field + * @param wordPanel + * @returns {Promise} + */ +async function submitFieldToggle(wordPanel) { + let row = Number(wordPanel.getAttribute('b-row')); + let column = Number(wordPanel.getAttribute('b-column')); + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!, $row:Int!, $column:Int!){ + bingo { + mutateLobby(id:$lobbyId) { + toggleGridField(location:{row:$row, column:$column}) { + submitted + grid { + bingo + } + } + } + } + }`, {lobbyId: getLobbyParam(), row: row, column: column}); + + if (response.status === 200) { + wordPanel.setAttribute('b-sub', response.data.bingo.mutateLobby.toggleGridField.submitted); + if (response.data.bingo.mutateLobby.toggleGridField.grid.bingo) + document.querySelector('#container-bingo-button').setAttribute('class', ''); + else + document.querySelector('#container-bingo-button').setAttribute('class', 'hidden'); + } else { + console.error(response); + showError('Error when submitting field toggle'); + } +} + +/** + * Submits bingo + * @returns {Promise} + */ +async function submitBingo() { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + submitBingo { + winner { + id + username + } + status + start + finish + } + } + } + }`, {lobbyId: getLobbyParam()}); + + if (response.status === 200 && response.data.bingo.mutateLobby.submitBingo) { + let round = response.data.bingo.mutateLobby.submitBingo; + displayWinner(round); + } else { + console.error(response); + showError('Failed to submit bingo'); + } +} + /** * Displays the winner of the game in a popup. - * @param name {String} - the name of the winner + * @param roundInfo {Object} - the round object as returned by graphql */ -function displayWinner(name) { +function displayWinner(roundInfo) { + let name = roundInfo.winner.username; let winnerDiv = document.createElement('div'); let greyoverDiv = document.createElement('div'); winnerDiv.setAttribute('class', 'popup'); winnerDiv.innerHTML = `

${name} has won!

- - + `; greyoverDiv.setAttribute('class', 'greyover'); //winnerDiv.onclick = () => { @@ -69,18 +291,37 @@ function displayWinner(name) { * @param errorMessage */ function showError(errorMessage) { - let errorDiv = document.createElement('div'); - errorDiv.setAttribute('class', 'errorDiv'); - errorDiv.innerHTML = `${errorMessage}`; - let contCont = document.querySelector('#content-container'); - if (contCont) { - contCont.appendChild(errorDiv); + // TODO: Implement +} + +/** + * Loads information about the rounds winner and the round stats. + * @returns {Promise} + */ +async function loadWinnerInfo() { + let response = await postGraphqlQuery(` + query($lobbyId:ID!) { + bingo { + lobby(id:$lobbyId) { + currentRound { + status + winner { + id + username + } + start + finish + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let roundInfo = response.data.bingo.lobby.currentRound; + displayWinner(roundInfo); } else { - alert(errorMessage); + console.error(response); + showError('Failed to get round information'); } - setTimeout(() => { - errorDiv.remove(); - }, 10000); } /** @@ -93,7 +334,7 @@ function addChatMessage(messageObject) { msgSpan.setAttribute('msg-id', messageObject.id); if (messageObject.type === "USER") { msgSpan.innerHTML = ` - ${messageObject.username}: + ${messageObject.author.username}: ${messageObject.htmlContent}`; } else { msgSpan.innerHTML = ` @@ -105,11 +346,232 @@ function addChatMessage(messageObject) { chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom } +/** + * Adds a player to the player view + * @param player + */ +function addPlayer(player) { + let playerContainer = document.createElement('div'); + playerContainer.setAttribute('class', 'playerEntryContainer'); + playerContainer.setAttribute('b-pid', player.id); + playerContainer.innerHTML = `${player.username}`; + document.querySelector('#player-list').appendChild(playerContainer); +} + +/** + * Refreshes the bingo chat + * @returns {Promise} + */ +async function refreshChat() { + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + messages { + id + type + htmlContent + author { + username + } + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let messages = response.data.bingo.lobby.messages; + for (let message of messages) + if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) + addChatMessage(message); + } else { + showError('Failed to refresh messages'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh messages'); + console.error(err); + } + console.log('Refresh Chat'); +} + +/** + * Refreshes the player list + * @returns {Promise} + */ +async function refreshPlayers() { + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + players { + id + username + wins(lobbyId:$lobbyId) + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let players = response.data.bingo.lobby.players; + for (let player of players) + if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) + addPlayer(player); + } else { + showError('Failed to refresh players'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh players'); + console.error(err); + } +} + +/** + * Removes players that are not existent in the player array + * @param players {Array} - player id response of graphql + */ +function removeLeftPlayers(players) { + for (let playerEntry of document.querySelectorAll('.playerEntryContainer')) + if (!players.find(x => (x.id === playerEntry.getAttribute('b-pid')))) + playerEntry.remove(); +} + +/** + * Refreshes if a player-refresh is needed. + * Removes players that are not in the lobby anyomre. + * @param players + */ +function checkPlayerRefresh(players) { + let playerRefresh = false; + removeLeftPlayers(players); + for (let player of players) + if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) + playerRefresh = true; + if (playerRefresh) + refreshPlayers(); +} + +/** + * Checks if messages need to be refreshed and does it if it needs to. + * @param messages + */ +function checkMessageRefresh(messages) { + let messageRefresh = false; + for (let message of messages) + if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) + messageRefresh = true; + if (messageRefresh) + refreshChat(); +} + +/** + * refreshes the lobby and calls itself with a timeout + * @returns {Promise} + */ +async function refreshLobby() { + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!){ + bingo { + lobby(id:$lobbyId) { + players { + id + } + messages { + id + } + currentRound { + id + } + words { + content + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let {players, messages, currentRound} = response.data.bingo.lobby; + checkPlayerRefresh(players); + checkMessageRefresh(messages); + let wordContainer = document.querySelector('#bingo-words'); + + if (wordContainer) + wordContainer.innerHTML = ` + ${response.data.bingo.lobby.words.map(x => x.content).join('')}`; + + if (currentRound && currentRound.id && Number(currentRound.id) !== Number(getRoundParam())) + insertParam('r', currentRound.id); + + } else { + showError('Failed to refresh lobby'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh lobby'); + console.error(err); + } finally { + setTimeout(refreshLobby, 1000); + } +} + +/** + * Checks the status of the lobby and the current round. + * @returns {Promise} + */ +async function refreshRound() { + let roundOver = false; + try { + let response = await postGraphqlQuery(` + query($lobbyId:ID!) { + bingo { + lobby(id:$lobbyId) { + players { + id + } + messages { + id + } + currentRound { + id + status + } + } + } + }`, {lobbyId: getLobbyParam()}); + if (response.status === 200) { + let {players, messages, currentRound} = response.data.bingo.lobby; + + checkPlayerRefresh(players); + checkMessageRefresh(messages); + if (!currentRound || currentRound.status === "FINISHED") { + roundOver = true; + await loadWinnerInfo(); + } + } else { + showError('Failed to refresh round'); + console.error(response); + } + } catch (err) { + showError('Failed to refresh round'); + console.error(err); + } finally { + if (!roundOver) + setTimeout(refreshRound, 1000); + } +} window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { promiseRejectionEvent.promise.catch(err => console.log(err)); showError('Connection problems... Is the server down?'); }); -window.onload = () => { -}; +// prevent ctrl + s +window.addEventListener("keydown", async (e) => { + if (e.which === 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) { + e.preventDefault(); + if (document.querySelector('#input-bingo-words')) + await setLobbyWords(getLobbyWords()); + } +}, false); diff --git a/public/stylesheets/sass/animations.sass b/public/stylesheets/sass/animations.sass index 69245b0..11717a4 100644 --- a/public/stylesheets/sass/animations.sass +++ b/public/stylesheets/sass/animations.sass @@ -8,8 +8,8 @@ @keyframes pulse-opacity 0% - opacity: 0.8 + opacity: 0.6 50% opacity: 1 100% - opacity: 0.8 + opacity: 0.6 diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 9596e6a..fd401eb 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -1,279 +1,158 @@ @import ../mixins @import ../vars -textarea - @include default-element - display: block - margin: 1rem - border-radius: 0 - font-size: 0.8em - background-color: lighten($primary, 15%) +//@media(max-device-width: 641px) -#word-count - margin: 1rem +//@media(min-device-width: 641px) -@media(max-device-width: 641px) - textarea - height: 80% - width: calc(100% - 2rem) - #words-container - width: 100% - height: 80% - #content-container - grid-template-columns: 0 100% !important - grid-template-rows: 10% 40% 40% 10% !important +.popup + @include default-element + position: fixed + display: grid + height: calc(50% - 1rem) + width: calc(40% - 1rem) + top: 25% + left: 30% + text-align: center + vertical-align: middle + padding: 1rem + z-index: 1000 - #players-container, #chat-container - display: none !important - padding: 0 + button + margin: 1rem + font-size: 2rem - .errorDiv - grid-column-start: 1 !important - grid-column-end: 4 !important +.greyover + width: 100% + height: 100% + position: fixed + z-index: 99 + top: 0 + left: 0 + background-color: rgba(0, 0, 0, 0.5) - #content-container.displayChat - grid-template-columns: 100% 0 !important - grid-template-rows: 0 25% 65% 10% !important +.playerEntryContainer + @include default-element + padding: 0.5rem + margin: 0 0 1rem + width: calc(100% - 1rem - 2px) + border-radius: 0 + color: $primarySurface + border: 1px solid $inactive - #players-container, #chat-container - display: block !important +.chatMessage + display: block + padding: 0.2em - #words-container - display: none !important + .INFO + font-style: italic + color: $inactive - #hide-player-container-button - display: none + .ERROR + font-weight: bold + color: $error - .popup - width: calc(100% - 2rem) !important - left: 0 !important + .chatUsername + color: $inactive - #button-container - grid-column-start: 2 !important - grid-column-end: 3 !important +#container-chat + height: calc(100% - 2px) + #chat-content + height: calc(100% - 3rem) + width: 100% + overflow-y: auto + border: 1px solid $inactive + box-shadow: inset 0 0 1rem $primary + #chat-input + width: 100% + height: 3rem - #chat-button-container - display: inline-block - grid-row-start: 4 - grid-row-end: 4 - grid-column-start: 1 - grid-column-end: 4 - overflow: hidden - margin: 0 0.5rem +#container-lobby-settings + height: 100% - button - width: 100% - margin: 0.5rem 0 + h1 + height: 3rem + margin: 0.5rem + text-align: center - #chat-container .chatMessage - font-size: 1.2em + #input-bingo-words + width: 100% + height: calc(100% - 7rem) + margin: 0 -@media(min-device-width: 641px) - textarea - height: 80% - width: 50% - #words-container + #button-round-start, #button-leave + height: 3rem width: 100% - height: 100% - #chat-button-container - display: none -.number-input - width: 4rem - margin: 1rem + #bingo-words + padding: 1rem + width: calc(100% - 2rem) + height: calc(100% - 9rem) + margin: 0 + overflow-y: auto + box-shadow: inset 0 0 1rem $primary -#bingoheader - display: table - width: 100% + .bingoWord + display: list-item + list-style: none - div - display: table-cell - text-align: start +#container-players + height: 100% + width: 100% - .stretchDiv - text-align: end + h1 + height: 3rem + margin: 0.5rem + text-align: center - button - max-width: calc(100% - 2rem) - padding: 0.7rem 2rem + #player-list + padding: 1rem + width: calc(100% - 2rem) + height: calc(100% - 6rem) + overflow-y: auto -#words-container +#container-grid display: table + height: calc(100% - 2em) + width: calc(100% - 2em) + padding: 1em - .bingo-word-row + .bingoWordRow display: table-row - .bingo-word-panel + .bingoWordPanel @include default-element display: table-cell - padding: 1rem - transition-duration: 0.3s - max-width: 15rem - border-radius: 0 - 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%) cursor: pointer - .bingo-word-panel:active + .bingoWordPanel[b-sub='true'] + background-color: $success + + .bingoWordPanel:hover background-color: $primary - .bingo-word-panel[b-sub="true"] - background-color: forestgreen + .bingoWordPanel[b-sub='true']:hover + background-color: mix($primary, $success) -#bingo-button - transition-duration: 0.8s + .bingoWordPanel:active + background-color: mix($primary, $secondary) -#content-container - display: grid - grid-template-columns: 25% 75% - grid-template-rows: 10% 40% 40% 10% +#container-bingo-button height: 100% width: 100% - div - overflow: auto - - #button-container - grid-column-start: 1 - grid-column-end: 1 - grid-row-start: 1 - grid-row-end: 1 - display: grid - 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: 3 - - h1 - margin: 0 0 1rem 0 - - #words-container - grid-column-start: 2 - grid-column-end: 3 - grid-row-start: 2 - grid-row-end: 4 - - #chat-container - grid-column-start: 1 - grid-column-end: 1 - grid-row-start: 3 - grid-row-end: 4 - height: calc(100% - 3px) - border: 1px solid $inactive - margin: 0 0.5rem - word-break: break-word - - #chat-content - height: calc(100% - 2.5rem) - background-color: $primary - overflow: auto - font-size: 0.8em - - .chatMessage - display: list-item - padding: 0.2rem - - .chatUsername - color: $inactive - - .ERROR - color: $error - - .INFO - color: $inactive - font-style: italic - - .chatMessageContent - img - width: 100% - height: auto - transition-duration: 0.5s - border-radius: 0.5em - img:hover - border-radius: 0 - - #chat-input - width: 100% - margin: 0 0 0 0 - height: 2.5rem - border-radius: 0 - - .errorDiv - grid-column-start: 2 - grid-column-end: 3 - grid-row-start: 4 - grid-row-end: 4 - background-color: $error - text-align: center - margin: 0.75rem 0 - border-radius: 1rem - height: calc(100% - 1.5rem) - display: table - - span - display: table-cell - font-size: 1.8rem - vertical-align: middle - -.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 - position: fixed - display: grid - height: calc(50% - 1rem) - width: calc(40% - 1rem) - top: 25% - left: 30% - text-align: center - vertical-align: middle - padding: 1rem - z-index: 1000 - button - margin: 1rem - font-size: 2rem + height: 100% + width: 100% -.greyover - width: 100% - height: 100% - position: fixed - z-index: 99 - top: 0 - left: 0 - background-color: rgba(0, 0, 0, 0.5) +/* main containers */ #container-bingo-create display: grid - grid-template-columns: 10% 80% 10% - grid-template-rows: 5% 10% 10% 70% 5% + grid-template: 5% 10% 10% 70% 5% /10% 80% 10% height: 100% width: 100% @@ -296,3 +175,43 @@ textarea button width: 100% + +#container-bingo-lobby + display: grid + grid-template: 5% 5% 85% 5% / 5% 30% 30% 30% 5% + height: 100% + width: 100% + + #lobby-title + @include gridPosition(2, 3, 2, 5) + margin: auto + + #container-players + @include gridPosition(3, 4, 2, 3) + background-color: lighten($primary, 5%) + + #container-lobby-settings + @include gridPosition(3, 4, 3, 4) + background-color: lighten($primary, 5%) + + #container-chat + @include gridPosition(3, 4, 4, 5) + background-color: lighten($primary, 5%) + +#container-bingo-round + display: grid + height: 100% + width: 100% + grid-template: 10% 42.5% 42.5% 5% / 25% 75% + + #container-players + @include gridPosition(2, 3, 1, 2) + + #container-chat + @include gridPosition(3, 4, 1, 2) + + #container-grid + @include gridPosition(2, 4, 2, 3) + + #container-bingo-button + @include gridPosition(1, 2, 1, 2) diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index e73f69f..c38e044 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -43,3 +43,9 @@ animation-name: pulse-opacity animation-duration: 5s animation-iteration-count: infinite + +.idle + background-color: $pending !important + animation-name: pulse-opacity + animation-duration: 2s + animation-iteration-count: infinite diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 16f0db6..2628fa9 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -53,6 +53,8 @@ input textarea background-color: lighten($primary, 15%) + color: $primarySurface + resize: none a color: $secondary diff --git a/routes/bingo.js b/routes/bingo.js index ea1b7f7..127c4f6 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -11,7 +11,7 @@ const express = require('express'), globals = require('../lib/globals'); let pgPool = globals.pgPool; -let bingoSessions = {}; +let playerNames = utils.getFileLines('./misc/usernames.txt').filter(x => (x && x.length > 0)); /** * Class to manage the bingo data in the database. @@ -373,11 +373,12 @@ class BingoDataManager { /** * Updates the rounds winner + * @param roundId {Number} - the id of the round * @param winnerId {Number} - the id of the winner * @returns {Promise<*>} */ - async setRoundWinner(winnerId) { - return await this._queryFirstResult(this.queries.setRoundWiner.sql, [winnerId]); + async setRoundWinner(roundId, winnerId) { + return await this._queryFirstResult(this.queries.setRoundWinner.sql, [roundId, winnerId]); } /** @@ -391,6 +392,16 @@ class BingoDataManager { return await this._queryFirstResult(this.queries.addUserMessage.sql, [playerId, lobbyId, messageContent]); } + /** + * Adds a message of type "INFO" to the lobby + * @param lobbyId {Number} - the id of the lobby + * @param messageContent {String} - the content of the info message + * @returns {Promise<*>} + */ + async addInfoMessage(lobbyId, messageContent) { + return await this._queryFirstResult(this.queries.addInfoMessage.sql, [lobbyId, messageContent]); + } + /** * Removes all words of a lobby * @param lobbyId {Number} - the id of the lobby @@ -793,7 +804,7 @@ class RoundWrapper { async setWinner(winnerId) { let status = await this.status(); if (status !== "FINISHED") { - let updateResult = await bdm.setRoundWinner(winnerId); + let updateResult = await bdm.setRoundWinner(this.id, winnerId); if (updateResult) await this.setFinished(); return true; @@ -816,11 +827,12 @@ class LobbyWrapper { /** * Loads information about the lobby if it hasn't been loaded yet + * @param [force] {Boolean} - forces a data reload * @returns {Promise} * @private */ - async _loadLobbyInfo() { - if (!this._infoLoaded) { + async _loadLobbyInfo(force) { + if (!this._infoLoaded && !force) { let row = await bdm.getLobbyInfo(this.id); this._assignProperties(row); } @@ -837,6 +849,7 @@ class LobbyWrapper { this.grid_size = row.grid_size; this.expire = row.expire; this.current_round = row.current_round; + this.last_round = row.last_round; this._infoLoaded = true; } } @@ -903,7 +916,7 @@ class LobbyWrapper { let messages = []; for (let row of rows) messages.push(new MessageWrapper(row)); - return messages; + return messages.reverse(); } /** @@ -999,6 +1012,27 @@ class LobbyWrapper { await bdm.addWordToLobby(this.id, word); } } + + /** + * Adds a player to the lobby. + * @param playerId + * @returns {Promise} + */ + async addPlayer(playerId) { + await bdm.addPlayerToLobby(playerId, this.id); + let username = await new PlayerWrapper(playerId).username(); + await bdm.addInfoMessage(this.id, `${username} joined.`); + await this._loadLobbyInfo(true); + } + + /** + * Returns if the lobby is in an active round + * @returns {Promise} + */ + async roundActive() { + let currentRound = await this.currentRound(); + return currentRound && (await currentRound.status()) === 'ACTIVE'; + } } @@ -1124,6 +1158,64 @@ function checkBingo(fg) { return diagonalBingo || verticalCheck || horizontalCheck; } +/** + * Gets player data for a lobby + * @param lobbyWrapper + * @returns {Promise} + */ +async function getPlayerData(lobbyWrapper) { + let playerData = []; + + for (let player of await lobbyWrapper.players()) + playerData.push({ + id: player.id, + wins: await player.wins({lobbyId: lobbyWrapper.id}), + username: await player.username()} + ); + return playerData; +} + +/** + * Gets data for all words of a lobby + * @param lobbyWrapper + * @returns {Promise} + */ +async function getWordsData(lobbyWrapper) { + let wordList = []; + + for (let word of await lobbyWrapper.words()) + wordList.push(await word.content()); + return wordList; +} + +/** + * Returns a completely resolved grid + * @param lobbyId + * @param playerId + * @returns {Promise<{bingo: boolean, fields: Array}>} + */ +async function getGridData(lobbyId, playerId) { + let playerWrapper = new PlayerWrapper(playerId); + let lobbyWrapper = new LobbyWrapper(lobbyId); + let grid = await playerWrapper.grid({lobbyId: lobbyId}); + let fields = await grid.fields(); + let fieldGrid = []; + + for (let i = 0; i < await lobbyWrapper.gridSize(); i++) { + fieldGrid[i] = []; + for (let j = 0; j < await lobbyWrapper.gridSize(); j++) { + let field = fields.find(x => (x.row === i && x.column === j)) + fieldGrid[i][j] = { + row: field.row, + column: field.column, + word: await field.word.content(), + submitted: field.submitted + }; + } + } + + return {fields: fieldGrid, bingo: await grid.bingo()}; +} // -- Router stuff @@ -1140,27 +1232,35 @@ router.use(async (req, res, next) => { next(); }); -router.get('/', (req, res) => { - let bingoUser = req.session.bingoUser; +router.get('/', async (req, res) => { + let playerId = req.session.bingoPlayerId; + if (!playerId) + req.session.bingoPlayerId = playerId = (await bdm.addPlayer(shuffleArray(playerNames)[0])).id; if (req.query.g) { let lobbyId = req.query.g; - - if (bingoSessions[gameId] && !bingoSessions[gameId].finished) { - bingoUser.game = gameId; - let bingoSession = bingoSessions[gameId]; - if (!bingoSession.users[bingoUser.id]) - bingoSession.addUser(bingoUser); - - if (!bingoUser.grids[gameId]) - bingoUser.grids[gameId] = generateWordGrid(bingoSession.gridSize, bingoSession.words); - - res.render('bingo/bingo-game', { - grid: bingoUser.grids[gameId].fieldGrid, - username: bingoUser.username, - players: bingoSession.players() - }); + let lobbyWrapper = new LobbyWrapper(lobbyId); + + if (!(await lobbyWrapper.roundActive())) { + if (!await lobbyWrapper.hasPlayer(playerId)) + await lobbyWrapper.addPlayer(playerId); + let playerData = await getPlayerData(lobbyWrapper); + let words = await getWordsData(lobbyWrapper); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-lobby', { + players: playerData, + isAdmin: (playerId === admin.id), + words: words, + wordString: words.join('\n')}); } else { - res.render('bingo/bingo-submit'); + if (await lobbyWrapper.hasPlayer(playerId)) { + let playerData = await getPlayerData(lobbyWrapper); + let grid = await getGridData(lobbyId, playerId); + res.render('bingo/bingo-round', {players: playerData, grid: grid}); + } else { + let playerData = await getPlayerData(lobbyWrapper); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-lobby', {players: playerData, isAdmin: (playerId === admin.id)}); + } } } else { res.render('bingo/bingo-create'); @@ -1174,7 +1274,8 @@ router.graphqlResolver = async (req, res) => { return { // queries - lobby: ({id}) => { + lobby: async ({id}) => { + await bdm.updateLobbyExpiration(id); return new LobbyWrapper(id); }, player: ({id}) => { diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql index b14176d..e84d6b3 100644 --- a/sql/bingo/createBingoTables.sql +++ b/sql/bingo/createBingoTables.sql @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS bingo.words ( CREATE TABLE IF NOT EXISTS bingo.messages ( id serial UNIQUE PRIMARY KEY, content varchar(255) NOT NULL, - player_id serial references bingo.players(id) ON DELETE SET NULL, + player_id integer, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, type varchar(8) DEFAULT 'USER' NOT NULL, created timestamp DEFAULT NOW() @@ -68,3 +68,7 @@ CREATE TABLE IF NOT EXISTS bingo.grid_words ( submitted boolean DEFAULT false, PRIMARY KEY (grid_id, grid_row, grid_column) ); + +-- altering + +ALTER TABLE bingo.messages ALTER COLUMN player_id DROP NOT NULL; diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index bb710f3..2638aa2 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -239,3 +239,10 @@ getWordsForGridId: # - {String} - the content of the message addUserMessage: sql: INSERT INTO bingo.messages (player_id, lobby_id, content) VALUES ($1, $2, $3) RETURNING *; + +# inserts a info message +# params: +# - {Number} - the id of the lobby +# - {String} - the content of the message +addInfoMessage: + sql: INSERT INTO bingo.messages (type, lobby_id, content) VALUES ('INFO', $1, $2) RETURNING *; diff --git a/views/bingo/bingo-chat.pug b/views/bingo/bingo-chat.pug new file mode 100644 index 0000000..74027b5 --- /dev/null +++ b/views/bingo/bingo-chat.pug @@ -0,0 +1,4 @@ +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-lobby.pug b/views/bingo/bingo-lobby.pug new file mode 100644 index 0000000..2ae7641 --- /dev/null +++ b/views/bingo/bingo-lobby.pug @@ -0,0 +1,19 @@ +extends bingo-layout + +block content + div(id='container-bingo-lobby') + h1(id='lobby-title') Bingo Lobby + include bingo-players + div(id='container-lobby-settings') + h1 Words + if isAdmin + textarea(id='input-bingo-words')= wordString + button(id='button-round-start' onclick='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 + + script(type='text/javascript') refreshLobby(); diff --git a/views/bingo/bingo-players.pug b/views/bingo/bingo-players.pug new file mode 100644 index 0000000..aefa5d3 --- /dev/null +++ b/views/bingo/bingo-players.pug @@ -0,0 +1,6 @@ +div(id='container-players') + h1 Players + div(id='player-list') + each player in players + div(class='playerEntryContainer', b-pid=`${player.id}`) + span(class='playerNameSpan')= player.username diff --git a/views/bingo/bingo-round.pug b/views/bingo/bingo-round.pug new file mode 100644 index 0000000..45aeb0a --- /dev/null +++ b/views/bingo/bingo-round.pug @@ -0,0 +1,24 @@ +include bingo-layout + +block content + div(id='container-bingo-round') + include bingo-players + include bingo-chat + if grid.bingo + div(id='container-bingo-button') + button(id='bingo-button' onclick='submitBingo()') Bingo! + else + div(id='container-bingo-button' class='hidden') + button(id='bingo-button' onclick='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 + script(type='text/javascript') refreshRound(); diff --git a/views/bingo/bingo-submit.pug b/views/bingo/bingo-submit.pug deleted file mode 100644 index db8c533..0000000 --- a/views/bingo/bingo-submit.pug +++ /dev/null @@ -1,13 +0,0 @@ -extends bingo-layout - -block content - div(id='bingoform') - div(id='bingoheader') - div - 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 (max 10,000)', maxlength=1000000) From 1a552a066149bef4779a4f4999a6254db0122f03 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 16 May 2019 00:13:13 +0200 Subject: [PATCH 30/42] Minor style fixes - fixed back to lobby button - fixed chat background --- public/javascripts/bingo-web.js | 1 + public/stylesheets/sass/bingo/style.sass | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index b9ad5fa..5fdbc37 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -166,6 +166,7 @@ async function setLobbyWords(words) { * @returns {Promise} */ async function startRound() { + let textinput = document.querySelector('#input-bingo-words'); let words = getLobbyWords(); let resultWords = await setLobbyWords(words); textinput.value = resultWords.map(x => x.content).join('\n'); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index fd401eb..3059e50 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -17,10 +17,15 @@ vertical-align: middle padding: 1rem z-index: 1000 + grid-template: 60% 40% / 100% + + h1 + @include gridPosition(1, 2, 1, 1) button margin: 1rem font-size: 2rem + @include gridPosition(2, 3, 1, 1) .greyover width: 100% @@ -59,7 +64,7 @@ height: calc(100% - 2px) #chat-content height: calc(100% - 3rem) - width: 100% + width: calc(100% - 2px) overflow-y: auto border: 1px solid $inactive box-shadow: inset 0 0 1rem $primary From d566505c3fd0554c74a90cc05e841e04104b5479 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 16 May 2019 11:19:29 +0200 Subject: [PATCH 31/42] Bingo lobby improvements - bingo added kicking of player - bingo added display of admin - bingo added grid size input --- CHANGELOG.md | 6 +- graphql/bingo.graphql | 2 +- misc/usernames.txt | 2 + public/favicon.ico | Bin 0 -> 102004 bytes public/javascripts/bingo-web.js | 80 ++++++++++++++++++----- public/stylesheets/sass/bingo/style.sass | 15 ++++- public/stylesheets/sass/style.sass | 13 +++- routes/bingo.js | 60 +++++++++++++---- views/bingo/bingo-lobby.pug | 2 + views/bingo/bingo-players.pug | 4 ++ 10 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 public/favicon.ico diff --git a/CHANGELOG.md b/CHANGELOG.md index f76a312..ff336d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - css for startpage (wip) - file for css animations - pug file for startpage +- bingo lobbys +- kick function for bingo +- grid size input ## Changed - changed export of `app.js` to the asynchronous init function that returns the app object - `bin/www` now calls the init function of `app.js` -- graphql api +- graphql bingo api +- bingo frontend ### Removed diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 9463835..9f077a2 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -19,7 +19,7 @@ type LobbyMutation { leave: Boolean "kicks a player from the lobby" - kickPlayer(playerId: ID!): BingoLobby + kickPlayer(pid: ID!): BingoPlayer "starts a round in a lobby if the user is the admin" startRound: BingoRound diff --git a/misc/usernames.txt b/misc/usernames.txt index 0ba10d3..66096b4 100644 --- a/misc/usernames.txt +++ b/misc/usernames.txt @@ -9,3 +9,5 @@ Angry Koala Dragonslayer Goblin Slayer useless Aqua +theP0wner79 +Pr0wn diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1fc95c2a45174125115da04e5e70456a271a395f GIT binary patch literal 102004 zcmeHQ3tUav8sDb~c{DDPQlgaJ52^GX?_48tM~$f%sZ>&E$|X7(qcN2E8SnDC6f=~e z$)oh}o;zF_hR7?1LXT53QK`hSto8l>-+z5;@3r@# zC?2Ikee@B9m%7y4P81bLQIxLktM&c3{4y@Hw0yljl%k^gQIwn8tMy@{C`!kXqWJvR z>s@)&xMlbN_(P7T(v70#f6b$Y;dOi{T=J?MFDMEhSI_*FiPrI-;X{U$ViVGt<)A@A z3mqNW?_O^&-WTEZ7B>6SPf?h^Ke9D2Xz|_a?ZwBY{r>0lbX0ib268qxN7jP}({1y& z``?89S>r!zU0r0Uqw~fx($Y}j_3OyN!~|g;ips6s&UqD3JgqIL4!ba!{!{V~@OKPn29J$r_pJ$)*&it=()Sy6$?Uawc;_0(Ov zX!j@QA7yQgyzud-PnbY!BlV^Hi1S(c?@9cb&4=!`Bl zH*7?IL?0|XG&-{l*ZT#wCTxugGIB0*PIB(&*qZxJk^hr(lQzqIP8&TvWR1-V?`wtm z!JvbGcU-Z*|8BdtzPG|L;Wu?EI&t(U%E`(?iK|zO>d}UFwCVgr3l;L_T>Z zPl|j2=a-pB;QuznAMg+25A+B07y2LW_P+}ikbjV|hq<}+Jl zur1kNVr4e zx7nW8)Sv}(=ZeOutvx^4ZiK$RXw2w8eL5o7OWI?jZETpv2ej=M*OTyuaIn25Wo#n# z{Nj4Zarq&f|9y`?-|P7M)rEog_m^C!z?W31S=ioOr_iUpPB9wu@DjELmKmwDxbAcs z$c8e~hI(vls4HC^N!Fe8b9x>Ydl0^l9X}?q!^#2m}IDfv*QwS68FtEnASuz=5o&|LEbv(Vbhj-b%0b z?cEFYZyBd@;Scz~)p`j2m*6DPAK(x0r>*@Y@Tb+>A$>XM59m*OaysA-@CW!K!GX5+ zU1UG#zo7pD{=oRXl^lTnS3-M1KS4iR89TIj%ZJJz8+_k%I5rE+cWGvhub;T$>z}tJ zu7B=NO+`ldUa_=&#ov)*wDMYYtMrG=tMu{kK&NtZ(dqpBzqJc^9&-)8zuf#&T3U)u zwPN2hr%$7_0|$`3nHkf);pVuvs{Z6Yaj2gks;{qaUM0ExPv&sD;(ObzxaZO8`iHQ* z{=vS!qPYqUcy6pbjm-UcP+W{g+u6M{{}W9agYhJDTO#Jnk-G)^_#g*k<9Eh?@wlY< zvkG$kzxDUYTE+kN^apa@7wRvOzscF(&9$HW-x2)J_W1*34um8wY*}wU2Sv;!wk$<; z$J2x0^Bz4w=4rWM<70a>PYV~&JWr;E<0K9ndH(GsM~t+Uw3)Ph1-3eDY|(~yRZr4p z()Pz3Y`fTpr0sA ztbsFTpw%l^qOW3O-`Gk%A9>*Cx`_`ua{I}(m;Y?rMthAX;oEOTe1P^VOpg`Lf50F3 z5AhHBN7n&>{zLyqazg0;q5oG92O$4Z@c19{Kgjfcirv6@4Tb?to`O2q}Q)sn~qDKCA5~Y4eh!9F=oUFG-l*TF0Bpk5o~k+m%4K&D#yQL^8`OXDvOcl zO(mZ}6+45Jlka6Y9@)06tPE{WNL>4g+KfN6Uvc+Lz{kcM!oPKN>*h^HD=I2NXV0ER=7xqy zUtL|%@9tG3*SLW{)^IQU4&o|-pw)BQ$$b6c_&1^S0DndDQ7-2qb7*D8zoZ24?;zuX zw&HIM@h_KnRCF!{_%n|0q5o&d2uiX3L;N%HhE=)%@z01O#6QG8;Q*`RS=RbO{4?SU z@elD2@lV46$p08MgLQoc=szQtp#L`5z;e5dRSW zkpIze0Q8?xGgzhlkpD@=5%ixX_DK=Mzf|6^N&_JOW5g2TAL1YKKN=2z{xfO@tF#~T zKdCr^{?o)hDT4Ty${SW`0OWs+SVH_m{6qdn!vWBLM$KTA_Cx+B6-Us2n%E~r5dTtn z!zvAc{Erb!h<}KG$p2_K0Q%3U8LZNN$p56`2>MSG`=kisUn*}{r2&xtF=7ev5AhHA z9}NdU{~0xdRoV~vpHv({|7l{M6hZt;O+V#6QG8NT)A?_?OBXR%w70 z;Ln5?#6Nxb!}w3mzj0(ecrdb3kXh*Hpid`EKvk8M3~0A37U{pKDH`0jFR~akNMUsq z`-J$1mzlOUGU(r5+xpTt~v6VDD;Sfu@ZgkH%p*`iTGxT^OlZ;za`eu`TqW>vZ4YBa4uen<55voU0scY zLLmd%EsL9)not$yi=yh+{5-S${E()anyBCDQ2671c+uRssIjrJrNg1*jR)oi1R%{` zy#W6=9^uwMfIqkJmTWEHFX<6(tpont!dtSnfWM?ixU~-Wa|>_D)&l;L9^uw{7#<^Q z<7<{3?)ndm|64`m#Ssz6P(!1`UH>KX@cgDuMcJ8|DChWblymGD7>y4C@wIAl&q>bL z{>bqae*ectPY;dI*GD6U4h3WI0r8OnuzxT3E2v$H@*msJqwx3#{N;X6UlvXR{_>w= zAqyUg-WKo&{0RpX%{w^Xhv9RG{|^I)_Z1I-Kj07g&s`2c{Bsw3(&qsFfIsLzcR2v@ z&t2?6|GAAl;1BqN{&SlHfIr}00@wdJd;f!Ae+2O7EdB)hDcA(S0nTxNU{0{_f^99f zCT#EPJ`|i+vA!o*6U+(r6gIWj>(zJ(SafV35R3`-1apdf$Pydj0?`A~Cy=~AJT`!3 z$MA(X*Ar{L2>X1|-#34h`EULe{lj~E71qE5-~sRecmO;A9$+T-6xFkq$twm^Qf*Wc;T~fe;`G5{h#}okN>r}sN!zHI^O{83)QuMB`*?8 zIGN)iXX~}oVaYDb z0tLs)@&^=mEACyqyx3fS*=N3;?{&UMT~_MPH{<_vvw2L(_KW-O|K!-%r+8{{@TN1d z1r_(au5O;L7W0d>k_B~0DZ^uY@%bqN$FiMD%Xy7G>Rw(MKlo|Bi;`cb<;p&*lCtAe z<_{2tD-|fa@+SAl32~2~tzG95F{ny2;!FQO{A}&+@4H(&c_072lB?#D%=Nu|AC$~P z$#=KAWh{LW9yjx_(_alfGc^x|XCzmbub%wz z#up1!hCV)VNGVz^N7enS%jvuGE^DxHPy;qRt=oy%zB@ z*WdPbYVf8*#tDy&_=VJ7$1Al>m5Weke}~-~)YHU-psHF8%4iGkyA=akweUXMM3wxN zm|nJ}Ap7pIwSs+FFAii{Z=IA_UfqxzWSf*w67V1{;7)X|%_>Q|;G{Z^~(-=JBUmDpuaqj`aIZRm{~HxeKD$23-7aB3*e z@Hx47b!u=__?pbx(3lXjn5t9pHBHv({tlOa>q4paw-9cRzIWo6nA|fjUKq{`j~!K5 zvGz~f2Y=NMoSRs4GcaSop**2Oc+PY6lu7$DQ#J(JME{zzeTG)*HKR|id4~GdFXdHs z>zaS0DD3n7{U*l!cdz#KuNE9_eWva24gt%W4oor#wfQ7F+|be`rM#(B zcj+mu3k$||qEu5%^X3|UA8@WVU3qO9&$rNOd-|f9uZ|B~^)zz()0$3Uj=MK3@SB!9 zvg~WmX9?Q*8bdz%2NmV|*~V0(-yM$pkoVZ$vFODUbLw8B{nwtF+X6@3o*b=gnYgWpmSM*f>pvo`y+dFmhj?@jVZf?vhNrF zv0wE8R*I(Ty7%bKmRy!#qU20J)?axLP|M-IMrK|w)l($Yf9 z%jX^TzVx)AsdruMiYfE!mlmdn{bn<5a&nz&XsBWEwr-i~CvAW76jt<_wK8k|<&7p@ zPqRj5+_JrW`}0esjx|jy9ybn}V>o44=c1^DquV`0|7jFZyM%uxFQiBQuznMN!18u< z+Qc7B)kEDpVyl99cB^u3tV#_wN_kjy$UxcBGo^1x+6L?0H&&g|6eg`xyV&*jyV`u7 z_gIx#XEz0v>_{E?>x0NTudO_d6OSVtc4ax|sYzi#zvI?5L-cz-oY>jCm#CNVjo|78jG@j|<2qkf+8pnfu-E^~0rsEA*~KjyeQ4a(!wy|eeij#2Ja*QiC;i+V)RRnu zmM;|qic zqq5^xQfGQh(XvWpJWu2S~{=9@dNJmc{ z8+mc3%QBT&N=~XyJ)L-WzH(Fw-dK|#Z#uIimbbXymWrOj9k+WupWkWL31#~;)=KNB zTIJ}T%jT>X?mLj5ZJKvVU>d=@xgsg3sY%$qc*2?t=YKnUlSIgU?Bp@&qh_u6Kj2aB AmH+?% literal 0 HcmV?d00001 diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 5fdbc37..cc3fff3 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -101,6 +101,32 @@ async function leaveLobby() { } } +/** + * Kicks a player by id. + * @param playerId + * @returns {Promise} + */ +async function kickPlayer(pid) { + let response = await postGraphqlQuery(` + mutation ($lobbyId: ID!, $playerId:ID!) { + bingo { + mutateLobby(id: $lobbyId) { + kickPlayer(pid: $playerId) { + id + } + } + } + } + `, {lobbyId: getLobbyParam(), playerId: pid}); + if (response.status === 200) { + let kickId = response.data.bingo.mutateLobby.kickPlayer.id; + document.querySelector(`.playerEntryContainer[b-pid='${kickId}'`).remove(); + } else { + showError('Failed to kick player!'); + console.error(response); + } +} + /** * Sends a message to the chat * @returns {Promise} @@ -138,21 +164,27 @@ async function sendChatMessage() { /** * Sets the words for the lobby * @param words + * @param gridSize * @returns {Promise} */ -async function setLobbyWords(words) { +async function setLobbySettings(words, gridSize) { + gridSize = Number(gridSize); let response = await postGraphqlQuery(` - mutation($lobbyId:ID!, $words:[String!]!){ + mutation ($lobbyId: ID!, $words: [String!]!, $gridSize:Int!) { bingo { - mutateLobby(id:$lobbyId) { - setWords(words:$words) { + mutateLobby(id: $lobbyId) { + setWords(words: $words) { words { content } } + setGridSize(gridSize: $gridSize) { + gridSize + } } } - }`, {lobbyId: getLobbyParam(), words: words}); + } + `, {lobbyId: getLobbyParam(), words: words, gridSize: gridSize}); if (response.status === 200) { return response.data.bingo.mutateLobby.setWords.words; } else { @@ -168,7 +200,8 @@ async function setLobbyWords(words) { async function startRound() { let textinput = document.querySelector('#input-bingo-words'); let words = getLobbyWords(); - let resultWords = await setLobbyWords(words); + let gridSize = document.querySelector('#input-grid-size').value || 3; + let resultWords = await setLobbySettings(words, gridSize); textinput.value = resultWords.map(x => x.content).join('\n'); let response = await postGraphqlQuery(` mutation($lobbyId:ID!){ @@ -351,11 +384,17 @@ function addChatMessage(messageObject) { * Adds a player to the player view * @param player */ -function addPlayer(player) { +function addPlayer(player, options) { let playerContainer = document.createElement('div'); playerContainer.setAttribute('class', 'playerEntryContainer'); playerContainer.setAttribute('b-pid', player.id); - playerContainer.innerHTML = `${player.username}`; + + if (options.isAdmin && player.id !== options.admin) + playerContainer.innerHTML = ``; + playerContainer.innerHTML += `${player.username}`; + + if (player.id === options.admin) + playerContainer.innerHTML += " 👑"; document.querySelector('#player-list').appendChild(playerContainer); } @@ -403,22 +442,31 @@ async function refreshChat() { async function refreshPlayers() { try { let response = await postGraphqlQuery(` - query($lobbyId:ID!){ + query ($lobbyId: ID!) { bingo { - lobby(id:$lobbyId) { + player { + id + } + lobby(id: $lobbyId) { players { id username - wins(lobbyId:$lobbyId) + wins(lobbyId: $lobbyId) + } + admin { + id } } } - }`, {lobbyId: getLobbyParam()}); + } + `, {lobbyId: getLobbyParam()}); if (response.status === 200) { let players = response.data.bingo.lobby.players; + let adminId = response.data.bingo.lobby.admin.id; + let isAdmin = response.data.bingo.player.id === adminId; for (let player of players) if (!document.querySelector(`.playerEntryContainer[b-pid="${player.id}"]`)) - addPlayer(player); + addPlayer(player, {admin: adminId, isAdmin: isAdmin}); } else { showError('Failed to refresh players'); console.error(response); @@ -572,7 +620,9 @@ window.addEventListener("unhandledrejection", function (promiseRejectionEvent) { window.addEventListener("keydown", async (e) => { if (e.which === 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) { e.preventDefault(); - if (document.querySelector('#input-bingo-words')) - await setLobbyWords(getLobbyWords()); + if (document.querySelector('#input-bingo-words')) { + let gridSize = document.querySelector('#input-grid-size').value || 3; + await setLobbySettings(getLobbyWords(), gridSize); + } } }, false); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 3059e50..a8ba59e 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -82,9 +82,13 @@ #input-bingo-words width: 100% - height: calc(100% - 7rem) + height: calc(100% - 10rem) margin: 0 + #input-grid-size + height: 3rem + width: 4rem + #button-round-start, #button-leave height: 3rem width: 100% @@ -116,6 +120,13 @@ height: calc(100% - 6rem) overflow-y: auto + .kickPlayerButton + background-color: #0000 + border: none + padding: 0 + margin: 0 0.5rem 0 0 + font-size: 1em + #container-grid display: table height: calc(100% - 2em) @@ -183,7 +194,7 @@ #container-bingo-lobby display: grid - grid-template: 5% 5% 85% 5% / 5% 30% 30% 30% 5% + grid-template: 0 10% 85% 5% / 5% 30% 30% 30% 5% height: 100% width: 100% diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index 2628fa9..ec096b1 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -46,11 +46,20 @@ button:active background-color: lighten($secondary, 15%) input - @include default-element + background-color: lighten($primary, 10%) + color: $primarySurface + border: 1px solid $inactive + transition-duration: 0.2s font-size: 1.2rem - background-color: lighten($primary, 15%) padding: 0.7rem +input:focus + background-color: lighten($primary, 15%) + border: 1px solid $primarySurface + +input[type='number'] + text-align: center + textarea background-color: lighten($primary, 15%) color: $primarySurface diff --git a/routes/bingo.js b/routes/bingo.js index 127c4f6..1bf6421 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -854,6 +854,15 @@ class LobbyWrapper { } } + /** + * Returns if the lobby exists (based on one loaded attribute) + * @returns {Promise} + */ + async exists() { + await this._loadLobbyInfo(); + return !!this.expire; + } + /** * returns the players in the lobby * @returns {Promise} @@ -1025,6 +1034,18 @@ class LobbyWrapper { await this._loadLobbyInfo(true); } + /** + * Removes a player from the lobby + * @param playerId + * @returns {Promise} + */ + async removePlayer(playerId) { + await bdm.removePlayerFromLobby(playerId, this.id); + let username = await new PlayerWrapper(playerId).username(); + await bdm.addInfoMessage(this.id, `${username} left.`); + await this._loadLobbyInfo(true); + } + /** * Returns if the lobby is in an active round * @returns {Promise} @@ -1236,9 +1257,9 @@ router.get('/', async (req, res) => { let playerId = req.session.bingoPlayerId; if (!playerId) req.session.bingoPlayerId = playerId = (await bdm.addPlayer(shuffleArray(playerNames)[0])).id; - if (req.query.g) { + let lobbyWrapper = new LobbyWrapper(req.query.g); + if (req.query.g && await lobbyWrapper.exists()) { let lobbyId = req.query.g; - let lobbyWrapper = new LobbyWrapper(lobbyId); if (!(await lobbyWrapper.roundActive())) { if (!await lobbyWrapper.hasPlayer(playerId)) @@ -1249,17 +1270,34 @@ router.get('/', async (req, res) => { res.render('bingo/bingo-lobby', { players: playerData, isAdmin: (playerId === admin.id), + adminId: admin.id, words: words, - wordString: words.join('\n')}); + wordString: words.join('\n'), + gridSize: await lobbyWrapper.gridSize() + }); } else { if (await lobbyWrapper.hasPlayer(playerId)) { let playerData = await getPlayerData(lobbyWrapper); let grid = await getGridData(lobbyId, playerId); - res.render('bingo/bingo-round', {players: playerData, grid: grid}); + let admin = await lobbyWrapper.admin(); + res.render('bingo/bingo-round', { + players: playerData, + grid: grid, + isAdmin: (playerId === admin.id), + adminId: admin.id + }); } else { let playerData = await getPlayerData(lobbyWrapper); let admin = await lobbyWrapper.admin(); - res.render('bingo/bingo-lobby', {players: playerData, isAdmin: (playerId === admin.id)}); + let words = await getWordsData(lobbyWrapper); + res.render('bingo/bingo-lobby', { + players: playerData, + isAdmin: (playerId === admin.id), + adminId: admin.id, + words: words, + wordString: words.join('\n'), + gridSize: await lobbyWrapper.gridSize() + }); } } } else { @@ -1314,15 +1352,15 @@ router.graphqlResolver = async (req, res) => { return { join: async () => { if (playerId) { - let result = await bdm.addPlayerToLobby(playerId, lobbyId); - return new LobbyWrapper(result.lobby_id); + await lobbyWrapper.addPlayer(playerId); + return lobbyWrapper; } else { res.status(400); } }, leave: async () => { if (playerId) { - await bdm.removePlayerFromLobby(playerId, lobbyId); + await lobbyWrapper.removePlayer(playerId); return true; } else { res.status(400); @@ -1331,8 +1369,8 @@ router.graphqlResolver = async (req, res) => { kickPlayer: async ({pid}) => { let admin = await lobbyWrapper.admin(); if (admin.id === playerId) { - let result = await bdm.removePlayerFromLobby(pid, lobbyId); - return new LobbyWrapper(result.id, result); + await lobbyWrapper.removePlayer(pid); + return new PlayerWrapper(pid); } }, startRound: async () => { @@ -1369,7 +1407,7 @@ router.graphqlResolver = async (req, res) => { submitBingo: async () => { let isBingo = await (await (new PlayerWrapper(playerId)).grid({lobbyId: lobbyId})).bingo(); let currentRound = await lobbyWrapper.currentRound(); - if (isBingo) { + if (isBingo && await lobbyWrapper.hasPlayer(playerId)) { let result = await currentRound.setWinner(playerId); if (result) return currentRound; diff --git a/views/bingo/bingo-lobby.pug b/views/bingo/bingo-lobby.pug index 2ae7641..e46351d 100644 --- a/views/bingo/bingo-lobby.pug +++ b/views/bingo/bingo-lobby.pug @@ -7,6 +7,8 @@ block content 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 else diff --git a/views/bingo/bingo-players.pug b/views/bingo/bingo-players.pug index aefa5d3..d17f2de 100644 --- a/views/bingo/bingo-players.pug +++ b/views/bingo/bingo-players.pug @@ -3,4 +3,8 @@ div(id='container-players') div(id='player-list') 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 + if player.id === adminId + span(class='adminSpan') 👑 From 6943719c7c26abf52eba4a63c8dc1030ea486166 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Thu, 16 May 2019 22:50:43 +0200 Subject: [PATCH 32/42] Changes to style - added backend limits - added frontend limits - added bingo statusbar - improved bingo stylesheet - added redirect on missing player --- CHANGELOG.md | 3 + app.js | 2 +- misc/usernames.txt | 4 + public/javascripts/bingo-web.js | 75 +++++++++++++--- public/javascripts/common.js | 11 ++- public/stylesheets/sass/bingo/style.sass | 95 +++++++++++++++----- public/stylesheets/sass/classes.sass | 19 ++-- public/stylesheets/sass/mixins.sass | 5 ++ public/stylesheets/sass/style.sass | 5 +- public/stylesheets/sass/vars.sass | 1 + routes/bingo.js | 60 ++++++++----- views/bingo/bingo-chat.pug | 4 - views/bingo/bingo-create.pug | 2 +- views/bingo/bingo-game.pug | 28 ------ views/bingo/bingo-lobby.pug | 15 ++-- views/bingo/bingo-round.pug | 32 +++---- views/bingo/includes/bingo-chat.pug | 10 +++ views/bingo/{ => includes}/bingo-layout.pug | 2 +- views/bingo/{ => includes}/bingo-players.pug | 8 +- views/bingo/includes/bingo-statusbar.pug | 7 ++ 20 files changed, 259 insertions(+), 129 deletions(-) delete mode 100644 views/bingo/bingo-chat.pug delete mode 100644 views/bingo/bingo-game.pug create mode 100644 views/bingo/includes/bingo-chat.pug rename views/bingo/{ => includes}/bingo-layout.pug (87%) rename views/bingo/{ => includes}/bingo-players.pug (55%) create mode 100644 views/bingo/includes/bingo-statusbar.pug 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 From 73ff02be6744255b895b548b980385a8cc938e1a Mon Sep 17 00:00:00 2001 From: Julius Date: Fri, 17 May 2019 10:30:51 +0200 Subject: [PATCH 33/42] Style fixes - fixed image and linebreaks in chat - changed ids for messages and words to bigserial --- app.js | 2 +- public/stylesheets/sass/bingo/style.sass | 7 +++++++ sql/bingo/createBingoTables.sql | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index 771587b..3447de8 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: true, + watchFiles: (process.env.NODE_ENV !== 'development'), logToConsole: true })); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 676a751..1ee72a7 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -54,6 +54,7 @@ .chatMessage display: block padding: 0.2em + word-break: break-word .INFO font-style: italic @@ -66,12 +67,18 @@ .chatUsername color: $inactive + img + width: 100% + height: auto + border-radius: 0.2em + #container-chat height: calc(100% - 2px) #chat-content height: calc(100% - 3rem) width: calc(100% - 2px) overflow-y: auto + overflow-x: hide border: 1px solid $inactive box-shadow: inset 0 0 1rem $primary #chat-input diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql index e84d6b3..c6d7c3b 100644 --- a/sql/bingo/createBingoTables.sql +++ b/sql/bingo/createBingoTables.sql @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS bingo.lobby_players ( -- words table CREATE TABLE IF NOT EXISTS bingo.words ( - id serial UNIQUE PRIMARY KEY, + id bigserial UNIQUE PRIMARY KEY, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, heared integer DEFAULT 0 NOT NULL, content varchar(254) NOT NULL @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS bingo.words ( -- messages table CREATE TABLE IF NOT EXISTS bingo.messages ( - id serial UNIQUE PRIMARY KEY, + id bigserial UNIQUE PRIMARY KEY, content varchar(255) NOT NULL, player_id integer, lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, From 31c91c54ad57d307fe542c6edf7014f00ea6959f Mon Sep 17 00:00:00 2001 From: Julius Date: Fri, 17 May 2019 11:28:21 +0200 Subject: [PATCH 34/42] Frontend fixes - added bingo help command - added bingo ping command --- CHANGELOG.md | 2 + public/javascripts/bingo-web.js | 61 ++++++++++++++++++++---- public/stylesheets/sass/bingo/style.sass | 2 +- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 365161b..c2aa5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - kick function for bingo - grid size input - bingo status bar +- bingo chat commands ## Changed @@ -47,3 +48,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - mobile layout - code style issues - Bingo button not shown on refresh +- bingo chat style (images too large) diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index e940383..47d04a9 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -55,6 +55,20 @@ async function submitUsername() { } } +/** + * Function that displays the ping in the console. + * @returns {Promise} + */ +async function ping() { + let start = new Date().getTime(); + let response = await postGraphqlQuery(` + query { + time + }`); + console.log(`Ping: ${(new Date().getTime()) - start} ms`); + return (new Date().getTime()) - start; +} + /** * TODO: real join logic */ @@ -135,6 +149,43 @@ async function kickPlayer(pid) { } } +/** + * Executes a command + * @param message {String} - the message + */ +async function executeCommand(message) { + function reply(content) { + addChatMessage({content: content, htmlContent: content, type: 'INFO'}); + } + let jsStyle = document.querySelector('#js-style'); + message = message.replace(/\s/g, ''); + switch(message) { + case '/help': + reply(` + Commands:
+ /help - shows this help
+ /hideinfo - hides all info messages
+ /showinfo - shows all info messages
+ /ping - shows the current ping
+ `); + break; + case '/hideinfo': + jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {display: none}'; + break; + case '/showinfo': + jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {}'; + break; + case '/ping': + reply(`Ping: ${await ping()} ms`); + break; + default: + reply('Unknown command'); + break; + } + let chatContent = document.querySelector('#chat-content'); + chatContent.scrollTop = chatContent.scrollHeight; +} + /** * Sends a message to the chat * @returns {Promise} @@ -144,14 +195,8 @@ async function sendChatMessage() { if (messageInput.value && messageInput.value.length > 0) { let message = messageInput.value; messageInput.value = ''; - 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; + if (/^\/\.*/g.test(message)) { + await executeCommand(message); } else { let response = await postGraphqlQuery(` mutation($lobbyId:ID!, $message:String!){ diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 1ee72a7..213eae5 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -68,7 +68,7 @@ color: $inactive img - width: 100% + max-width: 100% height: auto border-radius: 0.2em From d0085a2bfd578469c08831342591a4f9c044f557 Mon Sep 17 00:00:00 2001 From: Julius Date: Fri, 17 May 2019 13:55:00 +0200 Subject: [PATCH 35/42] More commands - fixed backend error responses - added round abort command --- CHANGELOG.md | 1 + graphql/bingo.graphql | 3 +++ public/javascripts/bingo-web.js | 35 +++++++++++++++++++++++++++-- routes/bingo.js | 39 +++++++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2aa5f1..f0a1ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,3 +49,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - code style issues - Bingo button not shown on refresh - bingo chat style (images too large) +- backend now returns precise error messages diff --git a/graphql/bingo.graphql b/graphql/bingo.graphql index 9f077a2..e1047ef 100644 --- a/graphql/bingo.graphql +++ b/graphql/bingo.graphql @@ -24,6 +24,9 @@ type LobbyMutation { "starts a round in a lobby if the user is the admin" startRound: BingoRound + "sets the round status" + setRoundStatus(status: RoundStatus): BingoRound + "sets the new gridsize for the lobby" setGridSize(gridSize: Int!): BingoLobby diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 47d04a9..a4b464d 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -125,7 +125,7 @@ async function leaveLobby() { /** * Kicks a player by id. - * @param playerId + * @param pid * @returns {Promise} */ async function kickPlayer(pid) { @@ -167,6 +167,7 @@ async function executeCommand(message) { /hideinfo - hides all info messages
/showinfo - shows all info messages
/ping - shows the current ping
+ /abortround - aborts the current round
`); break; case '/hideinfo': @@ -178,6 +179,9 @@ async function executeCommand(message) { case '/ping': reply(`Ping: ${await ping()} ms`); break; + case '/abortround': + reply(await setRoundFinished()); + break; default: reply('Unknown command'); break; @@ -334,6 +338,30 @@ async function submitFieldToggle(wordPanel) { } } +/** + * Sets the round status to FINISHED + * @returns {Promise} + */ +async function setRoundFinished() { + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + setRoundStatus(status:FINISHED) { + status + } + } + } + }`, {lobbyId: getLobbyParam()}); + + if (response.status === 200 && response.data.bingo.mutateLobby.setRoundStatus) { + return 'Set round to finished'; + } else { + console.error(response); + showError('Failed to set round status'); + } +} + /** * Submits bingo * @returns {Promise} @@ -442,7 +470,10 @@ async function loadWinnerInfo() { }`, {lobbyId: getLobbyParam()}); if (response.status === 200) { let roundInfo = response.data.bingo.lobby.currentRound; - displayWinner(roundInfo); + if (roundInfo.winner) + displayWinner(roundInfo); + else + window.location.reload(); } else { console.error(response); showError('Failed to get round information'); diff --git a/routes/bingo.js b/routes/bingo.js index 7515a14..97f35b8 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1064,6 +1064,18 @@ class LobbyWrapper { let currentRound = await this.currentRound(); return currentRound && (await currentRound.status()) === 'ACTIVE'; } + + /** + * Sets the status of the current round + * @param status {String} - the status + * @returns {Promise} + */ + async setRoundStatus(status) { + let currentRound = await this.currentRound(); + await currentRound.updateStatus(status); + await bdm.addInfoMessage(this.id, `Admin set round status to ${status}`); + return currentRound; + } } @@ -1386,6 +1398,9 @@ router.graphqlResolver = async (req, res) => { if (admin.id === playerId) { await lobbyWrapper.removePlayer(pid); return new PlayerWrapper(pid); + } else { + res.status(403); + return new GraphQLError('You are not an admin'); } }, startRound: async () => { @@ -1393,6 +1408,18 @@ router.graphqlResolver = async (req, res) => { if (admin.id === playerId) { await lobbyWrapper.startNewRound(); return lobbyWrapper.currentRound(); + } else { + res.status(403); + return new GraphQLError('You are not an admin'); + } + }, + setRoundStatus: async ({status}) => { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + return await lobbyWrapper.setRoundStatus(status); + } else { + res.status(403); + return new GraphQLError('You are not an admin'); } }, setGridSize: async ({gridSize}) => { @@ -1400,6 +1427,9 @@ router.graphqlResolver = async (req, res) => { if (admin.id === playerId) { await lobbyWrapper.setGridSize(gridSize); return lobbyWrapper; + } else { + res.status(403); + return new GraphQLError('You are not an admin'); } }, setWords: async({words}) => { @@ -1410,6 +1440,7 @@ router.graphqlResolver = async (req, res) => { return lobbyWrapper; } else { res.status(413); // request entity too large + return new GraphQLError('Too many words'); } else res.status(403); // forbidden @@ -1421,6 +1452,7 @@ router.graphqlResolver = async (req, res) => { return new MessageWrapper(result); } else { res.status(401); // unautorized + return new GraphQLError('You are not in the lobby'); } }, submitBingo: async () => { @@ -1428,10 +1460,13 @@ router.graphqlResolver = async (req, res) => { let currentRound = await lobbyWrapper.currentRound(); if (isBingo && await lobbyWrapper.hasPlayer(playerId)) { let result = await currentRound.setWinner(playerId); - if (result) + let username = await new PlayerWrapper(playerId).username(); + if (result) { + await bdm.addInfoMessage(lobbyId, `**${username}** won!`); return currentRound; - else + } else { res.status(500); + } } else { res.status(400); return new GraphQLError('Bingo check failed. This is not a bingo!'); From 44db9f844c2b010b9d879c33b9c76d1458e3424e Mon Sep 17 00:00:00 2001 From: Julius Date: Fri, 17 May 2019 14:58:48 +0200 Subject: [PATCH 36/42] more bingo commands - added /username bingo command --- public/javascripts/bingo-web.js | 86 +++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index a4b464d..6f66821 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -33,7 +33,20 @@ async function submitUsername() { let username = unameInput.value.replace(/^\s+|\s+$/g, ''); if (username.length > 1) { - let response = await postGraphqlQuery(` + await setUsername(username); + } else { + showError('You need to provide a username (minimum 2 characters)!'); + return false; + } +} + +/** + * Sets the username for a user + * @param username {String} - the username + * @returns {Promise} + */ +async function setUsername(username) { + let response = await postGraphqlQuery(` mutation($username:String!) { bingo { setUsername(username: $username) { @@ -42,15 +55,11 @@ async function submitUsername() { } } }`, {username: username}); - if (response.status === 200) { - return true; - } else { - showError(`Failed to submit username. HTTP Error: ${response.status}`); - console.error(response); - return false; - } + if (response.status === 200) { + return true; } else { - showError('You need to provide a username (minimum 2 characters)!'); + showError(`Failed to submit username. HTTP Error: ${response.status}`); + console.error(response); return false; } } @@ -158,36 +167,49 @@ async function executeCommand(message) { addChatMessage({content: content, htmlContent: content, type: 'INFO'}); } let jsStyle = document.querySelector('#js-style'); - message = message.replace(/\s/g, ''); - switch(message) { - case '/help': - reply(` - Commands:
+ message = message.replace(/\s+$/g, ''); + let command = /(\/\w+) ?(.*)?/g.exec(message); + if (command && command.length >= 2) { + switch(command[1]) { + case '/help': + reply(` +
Commands:
/help - shows this help
/hideinfo - hides all info messages
/showinfo - shows all info messages
/ping - shows the current ping
+ /username {Username} - sets the username

+ Admin commands:
/abortround - aborts the current round
`); - break; - case '/hideinfo': - jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {display: none}'; - break; - case '/showinfo': - jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {}'; - break; - case '/ping': - reply(`Ping: ${await ping()} ms`); - break; - case '/abortround': - reply(await setRoundFinished()); - break; - default: - reply('Unknown command'); - break; + break; + case '/hideinfo': + jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {display: none}'; + break; + case '/showinfo': + jsStyle.innerHTML = '.chatMessage[msg-type="INFO"] {}'; + break; + case '/ping': + reply(`Ping: ${await ping()} ms`); + break; + case '/abortround': + reply(await setRoundFinished()); + break; + case '/username': + if (command[2]) { + await setUsername(command[2]); + reply(`Your username is ${command[2]} now.`) + } else { + reply('You need to provide a username'); + } + break; + default: + reply('Unknown command'); + break; + } + let chatContent = document.querySelector('#chat-content'); + chatContent.scrollTop = chatContent.scrollHeight; } - let chatContent = document.querySelector('#chat-content'); - chatContent.scrollTop = chatContent.scrollHeight; } /** From 3e39a023b9c0102f71307ddb9dcfce9c6d927fc6 Mon Sep 17 00:00:00 2001 From: Julius Date: Fri, 17 May 2019 16:15:50 +0200 Subject: [PATCH 37/42] Changed toggle message - changed heared/unheared to toggled/untoggled --- routes/bingo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/bingo.js b/routes/bingo.js index 97f35b8..4c53961 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -615,10 +615,10 @@ class GridWrapper { let word = await gridField.word.content(); if (gridField.submitted) await bdm.addInfoMessage(this.lobbyId, - `${username} heared "${word}"`); + `${username} toggled "${word}"`); else await bdm.addInfoMessage(this.lobbyId, - `${username} unheared "${word}"`); + `${username} untoggled "${word}"`); return gridField; } } From a5af6cbb19cb99987916663354c687d03408bc00 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Fri, 17 May 2019 21:35:37 +0200 Subject: [PATCH 38/42] Bug Fixes - setting words won't result in deleting all of them and resaving - words can now only be set when no round is active --- CHANGELOG.md | 2 + routes/bingo.js | 78 ++++++++++++++++++++++++++++++--- sql/bingo/createBingoTables.sql | 2 +- sql/bingo/queries.yaml | 13 ++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a1ea2..940886d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,3 +50,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bingo button not shown on refresh - bingo chat style (images too large) - backend now returns precise error messages +- setting words won't result in deleting all of them and resaving +- words can now only be set when no round is active diff --git a/routes/bingo.js b/routes/bingo.js index 4c53961..93cbc0e 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -236,6 +236,16 @@ class BingoDataManager { return await this._queryFirstResult(this.queries.addWord.sql, [lobbyId, word]); } + /** + * Removes a word from the lobby + * @param lobbyId {Number} - the id of the lobby + * @param wordId {Number} - the id of the word + * @returns {Promise<*>} + */ + async removeWordFromLobby(lobbyId, wordId) { + return await this._queryFirstResult(this.queries.removeLobbyWord.sql, [lobbyId, wordId]); + } + /** * Returns all words used in a lobby * @param lobbyId {Number} - the id of the lobby @@ -303,6 +313,15 @@ class BingoDataManager { return await this._queryFirstResult(this.queries.addGrid.sql, [playerId, lobbyId, roundId]); } + /** + * Clears all grids for a lobby. + * @param lobbyId {Number} - the id of the lobby + * @returns {Promise<*>} + */ + async clearGrids(lobbyId) { + return await this._queryFirstResult(this.queries.clearLobbyGrids.sql, [lobbyId]); + } + /** * Adds a word to a grid with specific location * @param gridId {Number} - the id of the gird @@ -1018,18 +1037,63 @@ class LobbyWrapper { return (result && result.player_id); } + /** + * Adds a word to the lobby + * @param word + * @returns {Promise} + */ + async addWord(word) { + await bdm.addWordToLobby(this.id, word); + } + + /** + * Removes a word from the lobby + * @param wordId + * @returns {Promise} + */ + async removeWord(wordId) { + await bdm.removeWordFromLobby(this.id, wordId); + } + /** * Sets the words of the lobby * @param words * @returns {Promise} */ async setWords(words) { - if (words.length > 0) { - await bdm.clearLobbyWords(this.id); - for (let word of words) - // eslint-disable-next-line no-await-in-loop - await bdm.addWordToLobby(this.id, word); + if (words.length > 0 && !await this.roundActive()) { + let {newWords, removedWords} = await this._filterWords(words); + for (let word of newWords) + await this.addWord(word); + for (let word of removedWords) + await this.removeWord(word.id); + } + } + + /** + * Filters the bingo words + * @param words + * @returns {Promise<{removedWords: *[], newWords: *[]}>} + * @private + */ + async _filterWords(words) { + let curWords = await this.words(); + let currentWords = []; + let currentWordContent = []; + for (let word of curWords) { + currentWordContent.push(await word.content()); + currentWords.push({ + id: word.id, + content: (await word.content()) + }); } + let newWords = words.filter(x => (!currentWordContent.includes(x))); + let removedWords = currentWords.filter(x => !words.includes(x.content)); + + return { + newWords: newWords, + removedWords: removedWords + }; } /** @@ -1074,6 +1138,9 @@ class LobbyWrapper { let currentRound = await this.currentRound(); await currentRound.updateStatus(status); await bdm.addInfoMessage(this.id, `Admin set round status to ${status}`); + + if (status === 'FINISHED') + await bdm.clearGrids(this.id); return currentRound; } } @@ -1463,6 +1530,7 @@ router.graphqlResolver = async (req, res) => { let username = await new PlayerWrapper(playerId).username(); if (result) { await bdm.addInfoMessage(lobbyId, `**${username}** won!`); + await bdm.clearGrids(lobbyId); return currentRound; } else { res.status(500); diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql index c6d7c3b..c6061d7 100644 --- a/sql/bingo/createBingoTables.sql +++ b/sql/bingo/createBingoTables.sql @@ -62,7 +62,7 @@ CREATE TABLE IF NOT EXISTS bingo.grids ( -- grids_words table CREATE TABLE IF NOT EXISTS bingo.grid_words ( grid_id serial references bingo.grids(id) ON DELETE CASCADE, - word_id serial references bingo.words(id) ON DELETE CASCADE, + word_id serial references bingo.words(id) ON DELETE RESTRICT, grid_row integer NOT NULL, grid_column integer NOT NULL, submitted boolean DEFAULT false, diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index 2638aa2..1056fbf 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -160,6 +160,13 @@ addWord: clearLobbyWords: sql: DELETE FROM bingo.words WHERE lobby_id = $1; +# deletes a word of a lobby +# params: +# - {Number} - the id of the lobby +# - {Number} - the id of the word +removeLobbyWord: + sql: DELETE FROM bingo.words WHERE lobby_id = $1 AND id = $2; + # returns all words for a bingo game (lobby) # params: # - {Number} - the id of the bingo lobby @@ -180,6 +187,12 @@ getWordInfo: addGrid: sql: INSERT INTO bingo.grids (player_id, lobby_id, round_id) VALUES ($1, $2, $3) RETURNING *; +# deletes all grids of a lobby +# params: +# - {Number} - the id of the lobby +clearLobbyGrids: + sql: DELETE FROM bingo.grids WHERE lobby_id = $1; + # returns the grid entry for a player and lobby id # params: # - {Number} - the id of the player From e35cfabc49480b43180dfc5c24cfe3ad876bbb2d Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 19 May 2019 12:09:28 +0200 Subject: [PATCH 39/42] Cookie Dialog - added a cookie dialog - removed riddle (temporary) - removed users (temporary) --- CHANGELOG.md | 2 + app.js | 10 ++- bin/www | 108 ++++++++++++----------- graphql/schema.graphql | 1 + lib/globals.js | 9 +- public/javascripts/common.js | 8 ++ public/stylesheets/sass/classes.sass | 17 ++++ public/stylesheets/sass/index/style.sass | 29 ++++++ routes/bingo.js | 14 +-- routes/index.js | 5 +- views/bingo/includes/bingo-layout.pug | 1 + views/includes/info-container.pug | 5 ++ views/index.pug | 10 ++- views/layout.pug | 4 +- 14 files changed, 157 insertions(+), 66 deletions(-) create mode 100644 public/stylesheets/sass/index/style.sass create mode 100644 views/includes/info-container.pug diff --git a/CHANGELOG.md b/CHANGELOG.md index 940886d..37cde06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - grid size input - bingo status bar - bingo chat commands +- cookie info dialog ## Changed @@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - sqlite3 sesssion storage - old frontend - old bingo pug files +- riddle and users (currently deactivated) ### Fixed diff --git a/app.js b/app.js index 3447de8..0719d11 100644 --- a/app.js +++ b/app.js @@ -25,7 +25,11 @@ async function init() { let graphqlResolver = async (request, response) => { return { time: Date.now(), - bingo: await bingoRouter.graphqlResolver(request, response) + bingo: await bingoRouter.graphqlResolver(request, response), + acceptCookies: () => { + request.session.acceptedCookies = true; + return true; + } }; }; // database setup @@ -65,8 +69,8 @@ async function init() { app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); - app.use('/users', usersRouter); - app.use(/\/riddle(\/.*)?/, riddleRouter); + //app.use('/users', usersRouter); + //app.use(/\/riddle(\/.*)?/, riddleRouter); app.use('/bingo', bingoRouter); app.use('/graphql', graphqlHTTP(async (request, response) => { return await { diff --git a/bin/www b/bin/www index 2a09075..7a326cc 100644 --- a/bin/www +++ b/bin/www @@ -13,12 +13,13 @@ const fsx = require('fs-extra'); let settings = {}; try { - settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); + settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); - if (fsx.existsSync('config.yaml')) - Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); + if (fsx.existsSync('config.yaml')) { + Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); + } } catch (err) { - console.error(err); + console.error(err); } /** @@ -28,40 +29,44 @@ try { let port = normalizePort(process.env.PORT || settings.port || '3000'); appInit().then((app) => { - app.set('port', port); + app.set('port', port); - /** - * Create HTTP server. - */ + /** + * Create HTTP server. + */ - let server = http.createServer(app); + let server = http.createServer(app); - /** - * Listen on provided port, on all network interfaces. - */ + /** + * Listen on provided port, on all network interfaces. + */ - server.listen(port); - server.on('error', (error) => onError(error)); - server.on('listening', () => onListening(server)); + server.listen(port); + server.on('error', (error) => onError(error)); + server.on('listening', () => onListening(server)); - /** - * Normalize a port into a number, string, or false. - */ + /** + * Normalize a port into a number, string, or false. + */ }).catch((err) => { - console.error(err.message); - console.error(err.stack); + console.error(err.message); + console.error(err.stack); }); function normalizePort(val) { - let port = parseInt(val, 10); + let port = parseInt(val, 10); - if (isNaN(port)) + if (isNaN(port)) // named pipe - return val; - if (port >= 0) + { + return val; + } + if (port >= 0) // port number - return port; - return false; + { + return port; + } + return false; } /** @@ -69,26 +74,27 @@ function normalizePort(val) { */ function onError(error) { - if (error.syscall !== 'listen') - throw error; - - let bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } + if (error.syscall !== 'listen') { + throw error; + } + + let bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } } /** @@ -96,9 +102,9 @@ function onError(error) { */ function onListening(server) { - let addr = server.address(); - let bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Listening on ' + bind); + let addr = server.address(); + let bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); } diff --git a/graphql/schema.graphql b/graphql/schema.graphql index a5ac58a..9d430c6 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -8,4 +8,5 @@ type Query { type Mutation { bingo: BingoMutation + acceptCookies: Boolean } diff --git a/lib/globals.js b/lib/globals.js index 29cb7ad..e4d7c0a 100644 --- a/lib/globals.js +++ b/lib/globals.js @@ -11,5 +11,12 @@ Object.assign(exports, { user: settings.postgres.user, password: settings.postgres.password, database: settings.postgres.database - }) + }), + cookieInfo: { + headline: 'This website uses cookies', + content: 'This website uses cookies to store your session data (like for bingo).', + onclick: 'acceptCookies()', + id: 'cookie-container', + button: 'All right!' + } }); diff --git a/public/javascripts/common.js b/public/javascripts/common.js index f9564b4..ac8a1fe 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -127,3 +127,11 @@ async function indicateStatus(func, indicatorSelector) { statusIndicator.setAttribute('status', 'error'); } } + +async function acceptCookies() { + await postGraphqlQuery(` + mutation { + acceptCookies + }`); + document.querySelector('#cookie-container').remove(); +} diff --git a/public/stylesheets/sass/classes.sass b/public/stylesheets/sass/classes.sass index cd283e0..6172c8d 100644 --- a/public/stylesheets/sass/classes.sass +++ b/public/stylesheets/sass/classes.sass @@ -50,3 +50,20 @@ animation-name: pulse-opacity animation-duration: 2s animation-iteration-count: infinite + + +.infoContainer + position: absolute + bottom: 0 + left: 0 + background: lighten($primary, 5%) + padding: 1rem + border: 1px solid $primarySurface + z-index: 9999 + + h1 + margin: 0 0 1rem + + button + display: block + margin: 1rem auto 0 diff --git a/public/stylesheets/sass/index/style.sass b/public/stylesheets/sass/index/style.sass new file mode 100644 index 0000000..0d17292 --- /dev/null +++ b/public/stylesheets/sass/index/style.sass @@ -0,0 +1,29 @@ +@import ../vars +@import ../mixins + +#content + display: grid + grid-template: 25% 50% 25% / 25% 50% 25% + height: 100% + width: 100% + position: absolute + top: 0 + left: 0 + +#info + @include gridPosition(1, 2, 2, 3) + margin: auto + + h1, p + margin: auto + +#bingo-button + background-color: $secondary + height: 100% + width: 50vh + border-radius: 25rem + margin: auto + @include gridPosition(2, 3, 2, 3) + +#bingo-button:hover + background-color: mix($secondary, $primary, 75%) diff --git a/routes/bingo.js b/routes/bingo.js index 93cbc0e..89101d1 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -1347,8 +1347,7 @@ 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; + let info = req.session.acceptedCookies? null: globals.cookieInfo; let lobbyWrapper = new LobbyWrapper(req.query.g); if (playerId && req.query.g && await lobbyWrapper.exists()) { let lobbyId = req.query.g; @@ -1365,7 +1364,8 @@ router.get('/', async (req, res) => { adminId: admin.id, words: words, wordString: words.join('\n'), - gridSize: await lobbyWrapper.gridSize() + gridSize: await lobbyWrapper.gridSize(), + info: info }); } else { if (await lobbyWrapper.hasPlayer(playerId)) { @@ -1376,7 +1376,8 @@ router.get('/', async (req, res) => { players: playerData, grid: grid, isAdmin: (playerId === admin.id), - adminId: admin.id + adminId: admin.id, + info: info }); } else { let playerData = await getPlayerData(lobbyWrapper); @@ -1388,12 +1389,13 @@ router.get('/', async (req, res) => { adminId: admin.id, words: words, wordString: words.join('\n'), - gridSize: await lobbyWrapper.gridSize() + gridSize: await lobbyWrapper.gridSize(), + info: info }); } } } else { - res.render('bingo/bingo-create'); + res.render('bingo/bingo-create', {info: info}); } }); diff --git a/routes/index.js b/routes/index.js index f4f0679..daaeb4b 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,12 @@ const express = require('express'); const router = express.Router(); +const globals = require('../lib/globals'); + /* GET home page. */ router.get('/', function(req, res) { - res.render('index', { title: 'Trivernis.net' }); + let info = req.session.acceptedCookies? null: globals.cookieInfo; + res.render('index', { title: 'Trivernis.net', info: info}); }); module.exports = router; diff --git a/views/bingo/includes/bingo-layout.pug b/views/bingo/includes/bingo-layout.pug index d2e2d91..70695ab 100644 --- a/views/bingo/includes/bingo-layout.pug +++ b/views/bingo/includes/bingo-layout.pug @@ -6,4 +6,5 @@ html link(rel='stylesheet', href='/sass/bingo/style.sass') base(target='_blank') body + include ../../includes/info-container block content diff --git a/views/includes/info-container.pug b/views/includes/info-container.pug new file mode 100644 index 0000000..26d5582 --- /dev/null +++ b/views/includes/info-container.pug @@ -0,0 +1,5 @@ +if info + div(class='infoContainer' id=info.id) + h1=info.headline + span= info.content + button(onclick=info.onclick)= info.button diff --git a/views/index.pug b/views/index.pug index 8b93234..4797e93 100644 --- a/views/index.pug +++ b/views/index.pug @@ -1,6 +1,10 @@ extends layout block content - h1= title - p Welcome to #{title} - button(onclick='window.location.href="/bingo"') Bingo + div(id='content') + div(id='info') + h1= title + p Welcome to #{title} + button(id='bingo-button' onclick='window.location.href="/bingo"') Bingo + + include includes/info-container diff --git a/views/layout.pug b/views/layout.pug index 6f767bf..32f1880 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -2,6 +2,8 @@ doctype html html head title= title - link(rel='stylesheet', href='/sass/style.sass') + link(rel='stylesheet' href='/sass/style.sass') + link(rel='stylesheet' href='/sass/index/style.sass') + script(type='text/javascript' src='/javascripts/common.js') body block content From 9d806a7935ff9bdd78d9da4003d653882f149c67 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 19 May 2019 12:30:41 +0200 Subject: [PATCH 40/42] Fixed username submission - setting a username now gives better feedback - old username appeares in input field on load - fixed bug where users with expired bingo session but still existent session couldn't set a username - fixed status indicator --- package-lock.json | 888 +++++++++++++++++++++++ package.json | 3 +- public/javascripts/bingo-web.js | 4 +- public/stylesheets/sass/bingo/style.sass | 3 + routes/bingo.js | 26 +- views/bingo/bingo-create.pug | 2 + 6 files changed, 918 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9643799..0d3d560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,12 @@ "uri-js": "^4.2.2" } }, + "ajv-keywords": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", + "dev": true + }, "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", @@ -198,6 +204,16 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -262,6 +278,50 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -471,6 +531,23 @@ "unset-value": "^1.0.0" } }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + }, + "dependencies": { + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + } + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -555,6 +632,12 @@ "upath": "^1.1.1" } }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -685,6 +768,12 @@ "babylon": "^6.18.0" } }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -784,6 +873,12 @@ } } }, + "debug-log": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", + "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", + "dev": true + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -800,6 +895,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -837,6 +941,28 @@ } } }, + "deglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/deglob/-/deglob-2.1.1.tgz", + "integrity": "sha512-2kjwuGGonL7gWE1XU4Fv79+vVzpoQCl0V+boMwWtOQJV2AGDabCwez++nB1Nli/8BabAfZQ/UuHPlp6AymKdWw==", + "dev": true, + "requires": { + "find-root": "^1.0.0", + "glob": "^7.0.5", + "ignore": "^3.0.9", + "pkg-config": "^1.1.0", + "run-parallel": "^1.1.2", + "uniq": "^1.0.1" + }, + "dependencies": { + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -909,6 +1035,31 @@ "is-arrayish": "^0.2.1" } }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -993,6 +1144,70 @@ } } }, + "eslint-config-standard": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz", + "integrity": "sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ==", + "dev": true + }, + "eslint-config-standard-jsx": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-6.0.2.tgz", + "integrity": "sha512-D+YWAoXw+2GIdbMBRAzWwr1ZtvnSf4n4yL0gKGg7ShUOGXkSOLerI17K4F6LdQMJPNMoWYqepzQD/fKY+tXNSg==", + "dev": true + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-module-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz", + "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-plugin-es": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz", + "integrity": "sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==", + "dev": true, + "requires": { + "eslint-utils": "^1.3.0", + "regexpp": "^2.0.1" + } + }, "eslint-plugin-graphql": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/eslint-plugin-graphql/-/eslint-plugin-graphql-3.0.3.tgz", @@ -1003,12 +1218,152 @@ "lodash": "^4.11.1" } }, + "eslint-plugin-import": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz", + "integrity": "sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==", + "dev": true, + "requires": { + "contains-path": "^0.1.0", + "debug": "^2.6.8", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.1", + "eslint-module-utils": "^2.2.0", + "has": "^1.0.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.3", + "read-pkg-up": "^2.0.0", + "resolve": "^1.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "eslint-plugin-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz", + "integrity": "sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw==", + "dev": true, + "requires": { + "eslint-plugin-es": "^1.3.1", + "eslint-utils": "^1.3.1", + "ignore": "^4.0.2", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "semver": "^5.5.0" + } + }, "eslint-plugin-promise": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz", "integrity": "sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==", "dev": true }, + "eslint-plugin-react": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz", + "integrity": "sha512-cVVyMadRyW7qsIUh3FHp3u6QHNhOgVrLQYdQEB1bPWBsgbNCHdFAeNMquBMCcZJu59eNthX053L70l7gRt4SCw==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.0.1", + "prop-types": "^15.6.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + } + } + }, + "eslint-plugin-standard": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz", + "integrity": "sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA==", + "dev": true + }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -1441,6 +1796,12 @@ } } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", @@ -2177,6 +2538,12 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -2462,6 +2829,12 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -2480,6 +2853,12 @@ } } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -2586,6 +2965,21 @@ "has": "^1.0.1" } }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -2656,6 +3050,12 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -2705,6 +3105,15 @@ "promise": "^7.0.1" } }, + "jsx-ast-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.1.0.tgz", + "integrity": "sha512-yDGDG2DS4JcqhA6blsuYbtsT09xL8AoLuUR2Gb5exrw7UEM19sBcOTq+YBBhrNbl0PUC4R4LnFu+dHg2HKeVvA==", + "dev": true, + "requires": { + "array-includes": "^3.0.3" + } + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -2753,6 +3162,24 @@ "strip-bom": "^2.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", @@ -2763,6 +3190,15 @@ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -3185,6 +3621,12 @@ } } }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -3280,6 +3722,30 @@ "os-tmpdir": "^1.0.0" } }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", @@ -3441,6 +3907,98 @@ "pinkie": "^2.0.0" } }, + "pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "pkg-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pkg-config/-/pkg-config-1.1.1.tgz", + "integrity": "sha1-VX7yLXPaPIg3EHdmxS6tq94pj+Q=", + "dev": true, + "requires": { + "debug-log": "^1.0.0", + "find-root": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + } + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -3494,6 +4052,17 @@ "asap": "~2.0.3" } }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -3671,6 +4240,12 @@ } } }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -3803,6 +4378,24 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, "resolve": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", @@ -3861,6 +4454,12 @@ "is-promise": "^2.1.0" } }, + "run-parallel": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", + "dev": true + }, "rxjs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", @@ -4272,6 +4871,283 @@ "tweetnacl": "~0.14.0" } }, + "standard": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/standard/-/standard-12.0.1.tgz", + "integrity": "sha512-UqdHjh87OG2gUrNCSM4QRLF5n9h3TFPwrCNyVlkqu31Hej0L/rc8hzKqVvkb2W3x0WMq7PzZdkLfEcBhVOR6lg==", + "dev": true, + "requires": { + "eslint": "~5.4.0", + "eslint-config-standard": "12.0.0", + "eslint-config-standard-jsx": "6.0.2", + "eslint-plugin-import": "~2.14.0", + "eslint-plugin-node": "~7.0.1", + "eslint-plugin-promise": "~4.0.0", + "eslint-plugin-react": "~7.11.1", + "eslint-plugin-standard": "~4.0.0", + "standard-engine": "~9.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "eslint": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.4.0.tgz", + "integrity": "sha512-UIpL91XGex3qtL6qwyCQJar2j3osKxK9e3ano3OcGEIRM4oWIpCkDg9x95AXEC2wMs7PnxzOkPZ2gq+tsMS9yg==", + "dev": true, + "requires": { + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.2", + "imurmurhash": "^0.1.4", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.5.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" + } + }, + "eslint-plugin-promise": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz", + "integrity": "sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==", + "dev": true + }, + "espree": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", + "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "table": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", + "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", + "dev": true, + "requires": { + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } + }, + "standard-engine": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-9.0.0.tgz", + "integrity": "sha512-ZfNfCWZ2Xq67VNvKMPiVMKHnMdvxYzvZkf1AH8/cw2NLDBm5LRsxMqvEJpsjLI/dUosZ3Z1d6JlHDp5rAvvk2w==", + "dev": true, + "requires": { + "deglob": "^2.1.0", + "get-stdin": "^6.0.0", + "minimist": "^1.1.0", + "pkg-conf": "^2.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -4392,6 +5268,12 @@ "has-flag": "^3.0.0" } }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + }, "table": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/table/-/table-5.3.3.tgz", @@ -4655,6 +5537,12 @@ } } }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index 48f6019..50a90ed 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "devDependencies": { "eslint": "^5.16.0", "eslint-plugin-graphql": "^3.0.3", - "eslint-plugin-promise": "^4.1.1" + "eslint-plugin-promise": "^4.1.1", + "standard": "^12.0.1" }, "eslintConfig": { "parserOptions": { diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 6f66821..f6ff42a 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -33,7 +33,7 @@ async function submitUsername() { let username = unameInput.value.replace(/^\s+|\s+$/g, ''); if (username.length > 1) { - await setUsername(username); + return await setUsername(username); } else { showError('You need to provide a username (minimum 2 characters)!'); return false; @@ -58,7 +58,7 @@ async function setUsername(username) { if (response.status === 200) { return true; } else { - showError(`Failed to submit username. HTTP Error: ${response.status}`); + showError(`Failed to submit username. Error: ${response.errors.join(', ')}`); console.error(response); return false; } diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 213eae5..9607d57 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -237,6 +237,9 @@ button width: 100% + #statusbar + @include gridPosition(5, 6, 1, 4) + #container-bingo-lobby @include fillWindow overflow: hidden diff --git a/routes/bingo.js b/routes/bingo.js index 89101d1..05636e0 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -670,7 +670,7 @@ class PlayerWrapper { /** * Loads all player information - * @returns {Promise} + * @returns {Promise} * @private */ async _loadPlayerInfo() { @@ -680,10 +680,21 @@ class PlayerWrapper { this._uname = result.username; this.expire = result.expire; this._infoLoaded = true; + return true; + } else { + return false; } } } + /** + * Returns if the player exists + * @returns {Promise} + */ + async exists() { + return await this._loadPlayerInfo(); + } + /** * Returns the grid for a specific lobby * @param lobbyId {Number} - the id of the lobby @@ -1317,7 +1328,7 @@ async function getGridData(lobbyId, playerId) { for (let i = 0; i < await lobbyWrapper.gridSize(); i++) { fieldGrid[i] = []; for (let j = 0; j < await lobbyWrapper.gridSize(); j++) { - let field = fields.find(x => (x.row === i && x.column === j)) + let field = fields.find(x => (x.row === i && x.column === j)); fieldGrid[i][j] = { row: field.row, column: field.column, @@ -1349,7 +1360,8 @@ router.get('/', async (req, res) => { let playerId = req.session.bingoPlayerId; let info = req.session.acceptedCookies? null: globals.cookieInfo; let lobbyWrapper = new LobbyWrapper(req.query.g); - if (playerId && req.query.g && await lobbyWrapper.exists()) { + let playerWrapper = new PlayerWrapper(playerId); + if (playerId && await playerWrapper.exists() && req.query.g && await lobbyWrapper.exists()) { let lobbyId = req.query.g; if (!(await lobbyWrapper.roundActive())) { @@ -1395,7 +1407,10 @@ router.get('/', async (req, res) => { } } } else { - res.render('bingo/bingo-create', {info: info}); + res.render('bingo/bingo-create', { + info: info, + username: await playerWrapper.username() + }); } }); @@ -1422,8 +1437,9 @@ router.graphqlResolver = async (req, res) => { // mutations setUsername: async ({username}) => { username = replaceTagSigns(username.substring(0, 30)); // only allow 30 characters + let playerWrapper = new PlayerWrapper(playerId); - if (!playerId) { + if (!playerId || !(await playerWrapper.exists())) { req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; playerId = req.session.bingoPlayerId; } else { diff --git a/views/bingo/bingo-create.pug b/views/bingo/bingo-create.pug index 7013507..bb836c3 100644 --- a/views/bingo/bingo-create.pug +++ b/views/bingo/bingo-create.pug @@ -6,6 +6,7 @@ block content input(id='input-username' type='text' placeholder='Enter your name' + value=username onkeydown='submitOnEnter(event, () => indicateStatus(submitUsername, "#username-status"))') button( id='submit-username' @@ -14,3 +15,4 @@ block content div(id='lobby-form') button(id='join-lobby' onclick='joinLobby()') Join Lobby button(id='create-lobby' onclick='createLobby()') Create Lobby + include includes/bingo-statusbar From c91712d36742e10808d7a634e18f766e4de166cc Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 19 May 2019 17:39:53 +0200 Subject: [PATCH 41/42] Serveral improvements and features - added notifications on inactive window - changed max grid size to 5 - shortened phrases that exceed 200 characters - improved sql performance - fixed join issues --- CHANGELOG.md | 13 +- app.js | 2 + public/javascripts/bingo-web.js | 165 +++++++++++++++-------- public/javascripts/common.js | 26 ++++ public/stylesheets/sass/bingo/style.sass | 22 +-- public/stylesheets/sass/index/style.sass | 4 +- public/stylesheets/sass/style.sass | 13 +- public/stylesheets/sass/vars.sass | 4 + routes/bingo.js | 137 ++++++++++++++----- routes/changelog.js | 17 +++ routes/index.js | 2 +- sql/bingo/createBingoTables.sql | 2 +- sql/bingo/queries.yaml | 6 + views/bingo/includes/bingo-chat.pug | 5 + views/bingo/includes/bingo-statusbar.pug | 8 +- views/changelog/changes.pug | 6 + views/index.pug | 1 + 17 files changed, 329 insertions(+), 104 deletions(-) create mode 100644 routes/changelog.js create mode 100644 views/changelog/changes.pug diff --git a/CHANGELOG.md b/CHANGELOG.md index 37cde06..694486b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,14 +29,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - bingo status bar - bingo chat commands - cookie info dialog +- chat and round notifications -## Changed +### Changed - changed export of `app.js` to the asynchronous init function that returns the app object - `bin/www` now calls the init function of `app.js` - graphql bingo api - bingo frontend - moved some bingo pug files to ./bingo/includes/ +- style of `code` +- font to Ubuntu and Ubuntu Monospace +- grid size limit to 5 +- improved sql grid word insertion query ### Removed @@ -54,3 +59,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - backend now returns precise error messages - setting words won't result in deleting all of them and resaving - words can now only be set when no round is active +- username allowing emojis +- username can have a length of 0 (now at least 1 character) +- mozilla didn't have a fancy scrollbar (no webkit browser) +- kicked users can join lobby on active round +- users can't join lobby on active round +- server crash on too big phrases diff --git a/app.js b/app.js index 0719d11..933f3c8 100644 --- a/app.js +++ b/app.js @@ -17,6 +17,7 @@ const createError = require('http-errors'), indexRouter = require('./routes/index'), usersRouter = require('./routes/users'), riddleRouter = require('./routes/riddle'), + changelogRouter = require('./routes/changelog'), bingoRouter = require('./routes/bingo'); @@ -72,6 +73,7 @@ async function init() { //app.use('/users', usersRouter); //app.use(/\/riddle(\/.*)?/, riddleRouter); app.use('/bingo', bingoRouter); + app.use('/changelog', changelogRouter); app.use('/graphql', graphqlHTTP(async (request, response) => { return await { schema: buildSchema(importSchema('./graphql/schema.graphql')), diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index f6ff42a..0e97835 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -1,4 +1,5 @@ /* eslint-disable no-unused-vars, no-undef */ + /** * Returns the value of the url-param 'g' * @returns {string} @@ -9,7 +10,6 @@ function getLobbyParam() { return matches[1]; else return ''; - } /** @@ -24,6 +24,21 @@ function getRoundParam() { return ''; } +/** + * Spawns a notification when the window is inactive (hidden). + * @param body + * @param title + */ +function spawnNotification(body, title) { + if (Notification.permission !== 'denied' && document[getHiddenNames().hidden]) { + let options = { + body: body, + icon: '/favicon.ico' + }; + let n = new Notification(title, options); + } +} + /** * Submits the value of the username-input to set the username. * @returns {Promise} @@ -46,7 +61,9 @@ async function submitUsername() { * @returns {Promise} */ async function setUsername(username) { - let response = await postGraphqlQuery(` + let uname = username.substring(0, 30).replace(/[^\w- ;[\]]/g, ''); + if (uname.length === username.length) { + let response = await postGraphqlQuery(` mutation($username:String!) { bingo { setUsername(username: $username) { @@ -54,13 +71,19 @@ async function setUsername(username) { username } } - }`, {username: username}); - if (response.status === 200) { - return true; + }`, {username: username}, '/graphql?g='+getLobbyParam()); + if (response.status === 200) { + return response.data.bingo.setUsername.username; + } else { + if (response.errors) + showError(response.errors[0].message); + else + showError(`Failed to submit username.`); + console.error(response); + return false; + } } else { - showError(`Failed to submit username. Error: ${response.errors.join(', ')}`); - console.error(response); - return false; + showError('Your username contains illegal characters.'); } } @@ -79,11 +102,16 @@ async function ping() { } /** - * TODO: real join logic + * Joins a lobby or says to create one if none is found + * @returns {Promise} */ async function joinLobby() { - await submitUsername(); - window.location.reload(); + if (getLobbyParam()) { + if (await submitUsername()) + window.location.reload(); + } else { + showError('No lobby found. Please create one.'); + } } /** @@ -91,22 +119,24 @@ async function joinLobby() { * @returns {Promise} */ async function createLobby() { - let response = await postGraphqlQuery(` - mutation { - bingo { - createLobby { - id + if (await submitUsername()) { + let response = await postGraphqlQuery(` + mutation { + bingo { + createLobby { + id + } + } + } + `); + if (response.status === 200 && response.data.bingo.createLobby) { + insertParam('g', response.data.bingo.createLobby.id); + return true; + } else { + showError('Failed to create Lobby. HTTP ERROR: ' + response.status); + console.error(response); + return false; } - } - } - `); - if (response.status === 200 && response.data.bingo.createLobby) { - insertParam('g', response.data.bingo.createLobby.id); - return true; - } else { - showError('Failed to create Lobby. HTTP ERROR: ' + response.status); - console.error(response); - return false; } } @@ -197,8 +227,8 @@ async function executeCommand(message) { break; case '/username': if (command[2]) { - await setUsername(command[2]); - reply(`Your username is ${command[2]} now.`) + let uname = await setUsername(command[2]); + reply(`Your username is ${uname} now.`); } else { reply('You need to provide a username'); } @@ -278,7 +308,10 @@ async function setLobbySettings(words, gridSize) { return response.data.bingo.mutateLobby.setWords.words; } else { console.error(response); - showError('Error when setting lobby words.'); + if (response.errors) + showError(response.errors[0].message); + else + showError('Error when submitting lobby settings.'); } } @@ -289,25 +322,31 @@ async function setLobbySettings(words, gridSize) { async function startRound() { let textinput = document.querySelector('#input-bingo-words'); let words = getLobbyWords(); - let gridSize = document.querySelector('#input-grid-size').value || 3; - let resultWords = await setLobbySettings(words, gridSize); - textinput.value = resultWords.map(x => x.content).join('\n'); - let response = await postGraphqlQuery(` - mutation($lobbyId:ID!){ - bingo { - mutateLobby(id:$lobbyId) { - startRound { - id - } - } - } - }`, {lobbyId: getLobbyParam()}); + if (words.length > 0) { + let gridSize = document.querySelector('#input-grid-size').value || 3; + let resultWords = await setLobbySettings(words, gridSize); + if (resultWords) { + textinput.value = resultWords.map(x => x.content).join('\n'); + let response = await postGraphqlQuery(` + mutation($lobbyId:ID!){ + bingo { + mutateLobby(id:$lobbyId) { + startRound { + id + } + } + } + }`, {lobbyId: getLobbyParam()}); - if (response.status === 200) { - insertParam('r', response.data.bingo.mutateLobby.startRound.id); + if (response.status === 200) { + insertParam('r', response.data.bingo.mutateLobby.startRound.id); + } else { + console.error(response); + showError('Error when starting round.'); + } + } } else { - console.error(response); - showError('Error when starting round.'); + throw new Error('No words provided.'); } } @@ -429,11 +468,9 @@ function displayWinner(roundInfo) { `; greyoverDiv.setAttribute('class', 'greyover'); - //winnerDiv.onclick = () => { - // window.location.reload(); - //}; document.body.append(greyoverDiv); document.body.appendChild(winnerDiv); + spawnNotification(`${name} has won!`, 'Bingo'); } /** @@ -471,7 +508,7 @@ async function statusWrap(func) { /** * Loads information about the rounds winner and the round stats. - * @returns {Promise} + * @returns {Promise} */ async function loadWinnerInfo() { let response = await postGraphqlQuery(` @@ -505,8 +542,9 @@ async function loadWinnerInfo() { /** * Adds a message to the chat * @param messageObject {Object} - the message object returned by graphql + * @param player {Number} - the id of the player */ -function addChatMessage(messageObject) { +function addChatMessage(messageObject, player) { let msgSpan = document.createElement('span'); msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('msg-type', messageObject.type); @@ -520,6 +558,8 @@ function addChatMessage(messageObject) { ${messageObject.htmlContent}`; } + if (messageObject.author && messageObject.author.id !== player) + spawnNotification(messageObject.content, messageObject.author.username); let chatContent = document.querySelector('#chat-content'); chatContent.appendChild(msgSpan); chatContent.scrollTop = chatContent.scrollHeight; // auto-scroll to bottom @@ -552,12 +592,17 @@ async function refreshChat() { let response = await postGraphqlQuery(` query($lobbyId:ID!){ bingo { + player { + id + } lobby(id:$lobbyId) { messages { id type htmlContent + content author { + id username } } @@ -568,7 +613,7 @@ async function refreshChat() { let messages = response.data.bingo.lobby.messages; for (let message of messages) if (!document.querySelector(`.chatMessage[msg-id="${message.id}"]`)) - addChatMessage(message); + addChatMessage(message, response.data.bingo.player.id); } else { showError('Failed to refresh messages'); console.error(response); @@ -678,6 +723,7 @@ async function refreshLobby() { } currentRound { id + status } words { content @@ -695,8 +741,10 @@ async function refreshLobby() { wordContainer.innerHTML = ` ${response.data.bingo.lobby.words.map(x => x.content).join('')}`; - if (currentRound && currentRound.id && Number(currentRound.id) !== Number(getRoundParam())) + if (currentRound && currentRound.status === 'ACTIVE' && Number(currentRound.id) !== Number(getRoundParam())) { insertParam('r', currentRound.id); + spawnNotification('The round started!', 'Bingo'); + } } else { showError('Failed to refresh lobby'); @@ -771,3 +819,14 @@ window.addEventListener("keydown", async (e) => { } } }, false); + +window.onload = async () => { + if ("Notification" in window) + if (Notification.permission !== 'denied') { + try { + await Notification.requestPermission(); + } catch (err) { + showError(err.message); + } + } +}; diff --git a/public/javascripts/common.js b/public/javascripts/common.js index ac8a1fe..c74fd23 100644 --- a/public/javascripts/common.js +++ b/public/javascripts/common.js @@ -128,6 +128,10 @@ async function indicateStatus(func, indicatorSelector) { } } +/** + * posts to accept cookies. + * @returns {Promise} + */ async function acceptCookies() { await postGraphqlQuery(` mutation { @@ -135,3 +139,25 @@ async function acceptCookies() { }`); document.querySelector('#cookie-container').remove(); } + +/** + * Gets the names for the windows hidden and visibility change events and properties + * @returns {{hidden: string, visibilityChange: string}} + */ +function getHiddenNames() { + let hidden, visibilityChange; + if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support + hidden = "hidden"; + visibilityChange = "visibilitychange"; + } else if (typeof document.msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; + } else if (typeof document.webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; + } + return { + hidden: hidden, + visibilityChange: visibilityChange + }; +} diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 9607d57..284ceac 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -44,9 +44,11 @@ border-radius: 0 color: $primarySurface border: 1px solid $inactive + position: relative .playerWins - margin-left: 1em + position: absolute + right: 0 .kickPlayerButton color: $inactive @@ -183,13 +185,18 @@ #statusbar display: grid - grid-template: 100% / 1rem calc(80% - 1rem) 20% + grid-template: 100% 0 / 1rem calc(75% - 1rem) 25% background-color: darken($primary, 5%) margin: 0.5rem 0 0 0 padding: 0.25rem vertical-align: middle font-size: 0.8em text-align: start + position: absolute + height: 1rem + bottom: 0 + left: 0 + width: calc(100% - 0.5rem) #status-indicator height: 1rem @@ -204,6 +211,8 @@ color: $errorText #container-info + @include gridPosition(1, 2, 3, 4) + text-align: left width: 100% height: 100% margin: auto @@ -237,9 +246,6 @@ button width: 100% - #statusbar - @include gridPosition(5, 6, 1, 4) - #container-bingo-lobby @include fillWindow overflow: hidden @@ -264,9 +270,6 @@ @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 @@ -287,6 +290,3 @@ #container-bingo-button @include gridPosition(1, 2, 1, 2) - - #statusbar - @include gridPosition(4, 5, 1, 3) diff --git a/public/stylesheets/sass/index/style.sass b/public/stylesheets/sass/index/style.sass index 0d17292..7b3a921 100644 --- a/public/stylesheets/sass/index/style.sass +++ b/public/stylesheets/sass/index/style.sass @@ -13,9 +13,11 @@ #info @include gridPosition(1, 2, 2, 3) margin: auto + text-align: center - h1, p + h1, p, a margin: auto + text-align: center #bingo-button background-color: $secondary diff --git a/public/stylesheets/sass/style.sass b/public/stylesheets/sass/style.sass index ffb21f7..9a0a478 100644 --- a/public/stylesheets/sass/style.sass +++ b/public/stylesheets/sass/style.sass @@ -29,7 +29,8 @@ body background-color: $primary color: $primarySurface - font-family: Arial, sans-serif + font-family: $fontRegular + scrollbar-color: $secondary lighten($primary, 5) button @include default-element @@ -52,6 +53,7 @@ input transition-duration: 0.2s font-size: 1.2rem padding: 0.7rem + font-family: $fontRegular input:focus background-color: lighten($primary, 15%) @@ -64,9 +66,10 @@ textarea background-color: lighten($primary, 15%) color: $primarySurface resize: none + font-family: $fontRegular a - color: $secondary + color: mix($secondary, $primarySurface, 75%) mark background-color: $secondary @@ -75,6 +78,12 @@ mark mark > a color: white +code + background-color: transparentize(darken($primary, 10%), 0.5) + border-radius: 0.25em + font-family: $fontCode + padding: 0 0.25em + ::-webkit-scrollbar width: 12px height: 12px diff --git a/public/stylesheets/sass/vars.sass b/public/stylesheets/sass/vars.sass index f35a129..6723255 100644 --- a/public/stylesheets/sass/vars.sass +++ b/public/stylesheets/sass/vars.sass @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap') + $primary: #223 $primarySurface: white $secondary: teal @@ -7,3 +9,5 @@ $error: #a00 $errorText: #f44 $success: #0a0 $pending: #aa0 +$fontRegular: 'Ubuntu', Arial, sans-serif +$fontCode: 'Ubuntu Mono', monospace diff --git a/routes/bingo.js b/routes/bingo.js index 05636e0..229602a 100644 --- a/routes/bingo.js +++ b/routes/bingo.js @@ -12,7 +12,6 @@ const express = require('express'), globals = require('../lib/globals'); let pgPool = globals.pgPool; -let playerNames = utils.getFileLines('./misc/usernames.txt').filter(x => (x && x.length > 0)); /** * Class to manage the bingo data in the database. @@ -333,6 +332,25 @@ class BingoDataManager { return await this._queryFirstResult(this.queries.addWordToGrid.sql, [gridId, wordId, row, column]); } + /** + * Adds words to the grid + * @param gridId {Number} - the id of the grid + * @param words {Array<{wordId: Number, row: Number, column:Number}>} + * @returns {Promise} + */ + async addWordsToGrid(gridId, words) { + let valueSql = buildSqlParameters(4, words.length, 0); + let values = []; + for (let word of words) { + values.push(gridId); + values.push(word.wordId); + values.push(word.row); + values.push(word.column); + } + return await this._queryFirstResult( + this.queries.addWordToGridStrip.sql + valueSql + ' RETURNING *', values); + } + /** * Returns all words in the grid with location * @param gridId {Number} - the id of the grid @@ -707,6 +725,22 @@ class PlayerWrapper { return new GridWrapper(result.id); } + /** + * Returns if the user has a valid grid. + * @param lobbyId + * @returns {Promise} + */ + async hasGrid(lobbyId) { + let grid = await this.grid({lobbyId: lobbyId}); + if (grid) { + let fields = await grid.fields(); + let lobbyWrapper = new LobbyWrapper(lobbyId); + return fields.length === (await lobbyWrapper.gridSize()) ** 2; + } else { + return false; + } + } + /** * Returns the username of the player * @returns {Promise} @@ -1006,10 +1040,11 @@ class LobbyWrapper { let gridId = (await bdm.addGrid(this.id, player.id, currentRound)).id; let gridContent = generateWordGrid(this.grid_size, words); + let gridWords = []; for (let i = 0; i < gridContent.length; i++) for (let j = 0; j < gridContent[i].length; j++) - // eslint-disable-next-line no-await-in-loop - await bdm.addWordToGrid(gridId, gridContent[i][j].id, i, j); + gridWords.push({wordId: gridContent[i][j].id, row: i, column: j}); + await bdm.addWordsToGrid(gridId, gridWords); } } @@ -1025,6 +1060,7 @@ class LobbyWrapper { await currentRound.setFinished(); await this._createRound(); await this._createGrids(); + await this.setRoundStatus('ACTIVE'); } } @@ -1073,6 +1109,7 @@ class LobbyWrapper { */ async setWords(words) { if (words.length > 0 && !await this.roundActive()) { + words = words.map(x => x.substring(0, 200)); let {newWords, removedWords} = await this._filterWords(words); for (let word of newWords) await this.addWord(word); @@ -1156,6 +1193,23 @@ class LobbyWrapper { } } +/** + * Returns the parameterized value sql for inserting. + * @param columnCount + * @param rowCount + * @param [offset] + * @returns {string} + */ +function buildSqlParameters(columnCount, rowCount, offset) { + let sql = ''; + for (let i = 0; i < rowCount; i++) { + sql += '('; + for (let j = 0; j < columnCount; j++) + sql += `$${(i*columnCount)+j+1+offset},`; + sql = sql.replace(/,$/, '') + '),'; + } + return sql.replace(/,$/, ''); +} /** * Replaces tag signs with html-escaped signs. @@ -1341,6 +1395,20 @@ async function getGridData(lobbyId, playerId) { return {fields: fieldGrid, bingo: await grid.bingo()}; } +/** + * Returns resolved message data. + * @param lobbyId + * @returns {Promise} + */ +async function getMessageData(lobbyId) { + let lobbyWrapper = new LobbyWrapper(lobbyId); + let messages = await lobbyWrapper.messages({limit: 20}); + let msgReturn = []; + for (let message of messages) + msgReturn.push(Object.assign(message, {username: await message.author.username()})); + return msgReturn; +} + // -- Router stuff @@ -1361,10 +1429,11 @@ router.get('/', async (req, res) => { let info = req.session.acceptedCookies? null: globals.cookieInfo; let lobbyWrapper = new LobbyWrapper(req.query.g); let playerWrapper = new PlayerWrapper(playerId); + if (playerId && await playerWrapper.exists() && req.query.g && await lobbyWrapper.exists()) { let lobbyId = req.query.g; - if (!(await lobbyWrapper.roundActive())) { + if (!(await lobbyWrapper.roundActive() && await playerWrapper.hasGrid(lobbyId))) { if (!await lobbyWrapper.hasPlayer(playerId)) await lobbyWrapper.addPlayer(playerId); let playerData = await getPlayerData(lobbyWrapper); @@ -1377,10 +1446,11 @@ router.get('/', async (req, res) => { words: words, wordString: words.join('\n'), gridSize: await lobbyWrapper.gridSize(), - info: info + info: info, + messages: await getMessageData(lobbyId) }); } else { - if (await lobbyWrapper.hasPlayer(playerId)) { + if (await lobbyWrapper.hasPlayer(playerId) && await playerWrapper.hasGrid(lobbyId)) { let playerData = await getPlayerData(lobbyWrapper); let grid = await getGridData(lobbyId, playerId); let admin = await lobbyWrapper.admin(); @@ -1389,21 +1459,11 @@ router.get('/', async (req, res) => { grid: grid, isAdmin: (playerId === admin.id), adminId: admin.id, - info: info + info: info, + messages: await getMessageData(lobbyId) }); } else { - let playerData = await getPlayerData(lobbyWrapper); - let admin = await lobbyWrapper.admin(); - let words = await getWordsData(lobbyWrapper); - res.render('bingo/bingo-lobby', { - players: playerData, - isAdmin: (playerId === admin.id), - adminId: admin.id, - words: words, - wordString: words.join('\n'), - gridSize: await lobbyWrapper.gridSize(), - info: info - }); + res.redirect('/bingo'); } } } else { @@ -1436,16 +1496,24 @@ router.graphqlResolver = async (req, res) => { }, // mutations setUsername: async ({username}) => { - username = replaceTagSigns(username.substring(0, 30)); // only allow 30 characters - let playerWrapper = new PlayerWrapper(playerId); + username = replaceTagSigns(username.substring(0, 30)).replace(/[^\w- ;[\]]/g, ''); // only allow 30 characters + if (username.length > 0) { + let playerWrapper = new PlayerWrapper(playerId); - if (!playerId || !(await playerWrapper.exists())) { - req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; - playerId = req.session.bingoPlayerId; + if (!playerId || !(await playerWrapper.exists())) { + req.session.bingoPlayerId = (await bdm.addPlayer(username)).id; + playerId = req.session.bingoPlayerId; + } else { + let oldName = await playerWrapper.username(); + await bdm.updatePlayerUsername(playerId, username); + if (req.query.g) + await bdm.addInfoMessage(req.query.g, `${oldName} changed username to ${username}`); + } + return new PlayerWrapper(playerId); } else { - await bdm.updatePlayerUsername(playerId, username); + res.status(400); + return new GraphQLError('Username too short!'); } - return new PlayerWrapper(playerId); }, createLobby: async({gridSize}) => { if (playerId) @@ -1508,13 +1576,18 @@ router.graphqlResolver = async (req, res) => { } }, setGridSize: async ({gridSize}) => { - let admin = await lobbyWrapper.admin(); - if (admin.id === playerId) { - await lobbyWrapper.setGridSize(gridSize); - return lobbyWrapper; + if (gridSize > 0 && gridSize < 6) { + let admin = await lobbyWrapper.admin(); + if (admin.id === playerId) { + await lobbyWrapper.setGridSize(gridSize); + return lobbyWrapper; + } else { + res.status(403); + return new GraphQLError('You are not an admin'); + } } else { - res.status(403); - return new GraphQLError('You are not an admin'); + res.status(400); + return new GraphQLError('Grid size too big!'); } }, setWords: async({words}) => { diff --git a/routes/changelog.js b/routes/changelog.js new file mode 100644 index 0000000..d4648a5 --- /dev/null +++ b/routes/changelog.js @@ -0,0 +1,17 @@ +const express = require('express'), + router = express.Router(), + globals = require('../lib/globals'), + fsx = require('fs-extra'), + mdEmoji = require('markdown-it-emoji'), + md = require('markdown-it')() + .use(mdEmoji); + +let changelog = fsx.readFileSync('CHANGELOG.md', 'utf-8'); + +/* GET home page. */ +router.get('/', (req, res) => { + let info = req.session.acceptedCookies? null: globals.cookieInfo; + res.render('changelog/changes', { changelog: md.render(changelog), info: info}); +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js index daaeb4b..9693cbc 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,7 +6,7 @@ const globals = require('../lib/globals'); /* GET home page. */ router.get('/', function(req, res) { let info = req.session.acceptedCookies? null: globals.cookieInfo; - res.render('index', { title: 'Trivernis.net', info: info}); + res.render('index', { title: 'Trivernis.net', info: info, contact: 'mailto:trivernis@gmail.com'}); }); module.exports = router; diff --git a/sql/bingo/createBingoTables.sql b/sql/bingo/createBingoTables.sql index c6061d7..c512cb6 100644 --- a/sql/bingo/createBingoTables.sql +++ b/sql/bingo/createBingoTables.sql @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS bingo.rounds ( id serial UNIQUE PRIMARY KEY, start timestamp DEFAULT NOW(), finish timestamp, - status varchar(8) DEFAULT 'ACTIVE', + status varchar(8) DEFAULT 'BUILDING', lobby_id serial references bingo.lobbys(id) ON DELETE CASCADE, winner integer ); diff --git a/sql/bingo/queries.yaml b/sql/bingo/queries.yaml index 1056fbf..90bb6fd 100644 --- a/sql/bingo/queries.yaml +++ b/sql/bingo/queries.yaml @@ -218,6 +218,12 @@ getGridInfo: addWordToGrid: sql: INSERT INTO bingo.grid_words (grid_id, word_id, grid_row, grid_column) VALUES ($1, $2, $3, $4) RETURNING *; +# inserts grid-word connections into the database +# params: +# ! need to be set in the sql +addWordToGridStrip: + sql: INSERT INTO bingo.grid_words (grid_id, word_id, grid_row, grid_column) VALUES + # sets a bingo field to submitted = not submitted # params: # - {Number} - the id of the grid diff --git a/views/bingo/includes/bingo-chat.pug b/views/bingo/includes/bingo-chat.pug index a4eda8a..24f6ca8 100644 --- a/views/bingo/includes/bingo-chat.pug +++ b/views/bingo/includes/bingo-chat.pug @@ -1,6 +1,11 @@ div(id='container-chat') style(id='js-style') div(id='chat-content') + for message in messages + span.chatMessage(type=message.type msg-id=message.id) + if message.type === 'USER' + span.chatUsername= `${message.username}: ` + span(class=`chatMessageContent ${message.type}`)!= message.htmlContent input( id='chat-input' type='text' diff --git a/views/bingo/includes/bingo-statusbar.pug b/views/bingo/includes/bingo-statusbar.pug index ff83b5e..d9e6637 100644 --- a/views/bingo/includes/bingo-statusbar.pug +++ b/views/bingo/includes/bingo-statusbar.pug @@ -2,6 +2,10 @@ 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') Bug/Feature | - a(href='https://github.com/Trivernis/whooshy/issues') here + | + span | + | + | + a(href='mailto:trivernis@gmail.com') Contact diff --git a/views/changelog/changes.pug b/views/changelog/changes.pug new file mode 100644 index 0000000..7e5d66c --- /dev/null +++ b/views/changelog/changes.pug @@ -0,0 +1,6 @@ +html + head + include ../includes/head + body + include ../includes/info-container + div(id='changelog')!= changelog diff --git a/views/index.pug b/views/index.pug index 4797e93..8cb8ffc 100644 --- a/views/index.pug +++ b/views/index.pug @@ -5,6 +5,7 @@ block content div(id='info') h1= title p Welcome to #{title} + a(href=contact) Contact button(id='bingo-button' onclick='window.location.href="/bingo"') Bingo include includes/info-container From 2e31a513d36cc6329296038639affd585d4dd510 Mon Sep 17 00:00:00 2001 From: Trivernis Date: Sun, 19 May 2019 19:07:22 +0200 Subject: [PATCH 42/42] Release cleanup - code cleanup and release declaration --- CHANGELOG.md | 3 +-- app.js | 4 ++-- bin/www | 12 ++++-------- public/javascripts/bingo-web.js | 11 +++++------ public/stylesheets/sass/bingo/style.sass | 2 +- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 694486b..dae1e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - +## [0.1.0] - 2019-05-19 ### Added diff --git a/app.js b/app.js index 933f3c8..cebbece 100644 --- a/app.js +++ b/app.js @@ -15,8 +15,8 @@ const createError = require('http-errors'), settings = globals.settings, indexRouter = require('./routes/index'), - usersRouter = require('./routes/users'), - riddleRouter = require('./routes/riddle'), + //usersRouter = require('./routes/users'), + //riddleRouter = require('./routes/riddle'), changelogRouter = require('./routes/changelog'), bingoRouter = require('./routes/bingo'); diff --git a/bin/www b/bin/www index 7a326cc..f10421a 100644 --- a/bin/www +++ b/bin/www @@ -15,9 +15,8 @@ let settings = {}; try { settings = yaml.safeLoad(fsx.readFileSync('default-config.yaml')); - if (fsx.existsSync('config.yaml')) { + if (fsx.existsSync('config.yaml')) Object.assign(settings, yaml.safeLoad(fsx.readFileSync('config.yaml'))); - } } catch (err) { console.error(err); } @@ -58,14 +57,12 @@ function normalizePort(val) { if (isNaN(port)) // named pipe - { return val; - } + if (port >= 0) // port number - { return port; - } + return false; } @@ -74,9 +71,8 @@ function normalizePort(val) { */ function onError(error) { - if (error.syscall !== 'listen') { + if (error.syscall !== 'listen') throw error; - } let bind = typeof port === 'string' ? 'Pipe ' + port diff --git a/public/javascripts/bingo-web.js b/public/javascripts/bingo-web.js index 0e97835..90a8958 100644 --- a/public/javascripts/bingo-web.js +++ b/public/javascripts/bingo-web.js @@ -356,10 +356,9 @@ async function startRound() { */ function getLobbyWords() { let textinput = document.querySelector('#input-bingo-words'); - let words = textinput.value.replace(/[<>]/g, '').split('\n').filter((el) => { + return textinput.value.replace(/[<>]/g, '').split('\n').filter((el) => { return (!!el && el.length > 0); // remove empty strings and non-types from word array }); - return words; } /** @@ -549,14 +548,13 @@ function addChatMessage(messageObject, player) { msgSpan.setAttribute('class', 'chatMessage'); msgSpan.setAttribute('msg-type', messageObject.type); msgSpan.setAttribute('msg-id', messageObject.id); - if (messageObject.type === "USER") { + if (messageObject.type === "USER") msgSpan.innerHTML = ` ${messageObject.author.username}: ${messageObject.htmlContent}`; - } else { + else msgSpan.innerHTML = ` ${messageObject.htmlContent}`; - } if (messageObject.author && messageObject.author.id !== player) spawnNotification(messageObject.content, messageObject.author.username); @@ -567,7 +565,8 @@ function addChatMessage(messageObject, player) { /** * Adds a player to the player view - * @param player + * @param player {Object} - player as returned by graphql + * @param options {Object} - meta information */ function addPlayer(player, options) { let playerContainer = document.createElement('div'); diff --git a/public/stylesheets/sass/bingo/style.sass b/public/stylesheets/sass/bingo/style.sass index 284ceac..8ab627d 100644 --- a/public/stylesheets/sass/bingo/style.sass +++ b/public/stylesheets/sass/bingo/style.sass @@ -80,7 +80,7 @@ height: calc(100% - 3rem) width: calc(100% - 2px) overflow-y: auto - overflow-x: hide + overflow-x: hidden border: 1px solid $inactive box-shadow: inset 0 0 1rem $primary #chat-input