From 91f05c44df2169202197c9821b93f49802993dd9 Mon Sep 17 00:00:00 2001 From: marekpolakowski Date: Tue, 17 Jul 2018 17:01:54 +0200 Subject: [PATCH] OAB-18 create admin panel --- .env | 2 - .env.example | 3 ++ .gitignore | 1 + client/components/Game.jsx | 21 +++++++--- .../components/admin-panel/ActionButton.jsx | 28 +++++++++++++ client/components/admin-panel/AdminPanel.jsx | 31 +++++++++++++++ client/styles/admin_panel.scss | 39 +++++++++++++++++++ client/styles/game.scss | 1 + client/wrappers/withSocket.jsx | 22 +++++++++++ package.json | 1 + src/server.js | 23 ++++++----- .../handleAdminPanelBroadcasts.js | 32 +++++++++++++++ src/serverModules/handleSpinResult.js | 16 ++++++++ src/users.js | 4 +- src/utils/raspberryOnly.js | 7 ++++ webpack.config.js | 8 +++- yarn.lock | 18 +++++++++ 17 files changed, 239 insertions(+), 18 deletions(-) delete mode 100644 .env create mode 100644 .env.example create mode 100644 client/components/admin-panel/ActionButton.jsx create mode 100644 client/components/admin-panel/AdminPanel.jsx create mode 100644 client/styles/admin_panel.scss create mode 100644 client/wrappers/withSocket.jsx create mode 100644 src/serverModules/handleAdminPanelBroadcasts.js create mode 100644 src/serverModules/handleSpinResult.js create mode 100644 src/utils/raspberryOnly.js diff --git a/.env b/.env deleted file mode 100644 index 3d7e3d5..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -SLACK_WEBHOOK=https://hooks.slack.com/services/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 -SLACK_LOG_WEBHOOK=https://hooks.slack.com/services/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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e3ad662 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +SLACK_WEBHOOK=https://hooks.slack.com/123/456/789 +SLACK_LOG_WEBHOOK=https://hooks.slack.com/123/456/789 +RASPBERRY=0|1 diff --git a/.gitignore b/.gitignore index c7c28bb..1cc25d8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ images/* .DS_Store +.env diff --git a/client/components/Game.jsx b/client/components/Game.jsx index 49528a5..c2a36cf 100644 --- a/client/components/Game.jsx +++ b/client/components/Game.jsx @@ -4,14 +4,16 @@ import socketIOClient from 'socket.io-client' import { logResult, resultResponse } from '../modules/result' import { ToastContainer, toast } from 'react-toastify' +import withSocket from '@wrappers/withSocket' +import AdminPanel from '@components/admin-panel/AdminPanel' + const ROLLERS = ['left', 'center', 'right'] -export default class Game extends React.Component { +class Game extends React.Component { constructor(props) { super(props) - this.state = {} - this.socket = socketIOClient('http://localhost:3000') + this.state = { adminPanelActive: false } ROLLERS.forEach(roller => { this[roller] = React.createRef() @@ -19,8 +21,14 @@ export default class Game extends React.Component { } componentDidMount() { - this.socket.on('SPIN_REQUEST', forcedSpinTo => this._spinMachine(forcedSpinTo)) - this.socket.on('NOTIFY', (type, message) => toast[type](message)) + const { socket } = this.props; + socket.on('SPIN_REQUEST', forcedSpinTo => this._spinMachine(forcedSpinTo)) + socket.on('NOTIFY', (type, message) => toast[type](message)) + socket.on('SET_ADMIN_PANEL_ACTIVE', this._setAdminPanelActive.bind(this)) + } + + _setAdminPanelActive(value) { + this.setState({ adminPanelActive: value }) } _spinMachine(forcedSpinTo) { @@ -51,9 +59,12 @@ export default class Game extends React.Component { return (
+
{this._createRollers()}
) } } + +export default withSocket(Game); diff --git a/client/components/admin-panel/ActionButton.jsx b/client/components/admin-panel/ActionButton.jsx new file mode 100644 index 0000000..1ed5a19 --- /dev/null +++ b/client/components/admin-panel/ActionButton.jsx @@ -0,0 +1,28 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +import withSocket from '@wrappers/withSocket' + +class ActionButton extends Component { + _onClick(message) { + this.props.socket.emit(message) + } + + render() { + const { text, socketAction } = this.props + + return ( + this._onClick(socketAction)}>{text} + ) + } +} + +ActionButton.propTypes = { + text: PropTypes.string.isRequired, + socketAction: PropTypes.string.isRequired, + socket: PropTypes.shape({ + emit: PropTypes.func.isRequired + }).isRequired +} + +export default withSocket(ActionButton) diff --git a/client/components/admin-panel/AdminPanel.jsx b/client/components/admin-panel/AdminPanel.jsx new file mode 100644 index 0000000..c116627 --- /dev/null +++ b/client/components/admin-panel/AdminPanel.jsx @@ -0,0 +1,31 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +import ActionButton from '@components/admin-panel/ActionButton' +import '@styles/admin_panel' + +export default class AdminPanel extends Component { + _getWrapperClass(isOpen) { + const classes = ['admin-panel'] + if (isOpen) { classes.push('active') } + return classes.join(' ') + } + + render() { + const { isOpen } = this.props + + return ( +
+
+ + + +
+
+ ) + } +} + +AdminPanel.propTypes = { + isOpen: PropTypes.bool.isRequired +} diff --git a/client/styles/admin_panel.scss b/client/styles/admin_panel.scss new file mode 100644 index 0000000..9278d92 --- /dev/null +++ b/client/styles/admin_panel.scss @@ -0,0 +1,39 @@ +.admin-panel { + position: absolute; + width: 100%; + height: 100%; + z-index: 10; + padding: 15px; + box-sizing: border-box; + will-change: top; + transition: top 0.6s cubic-bezier(0.4, 0.2, 0, 1); + top: -100%; + background: #30373f; + + &.active { + top: 0%; + } + + &__actions { + margin: 0 -10px 20px; + display: flex; + + a { + font-family: Helvetica, sans-serif; + margin: 0 10px 20px; + padding: 0 20px; + background: #cccccc; + border: 1px solid #b1b2b3; + color: #6d6972; + border-radius: 3px; + font-size: 0.8em; + text-transform: uppercase; + line-height: 2.25em; + font-weight: bold; + } + + a, a:hover, a:active, a:focus, a:visited { + text-decoration: none; + } + } +} diff --git a/client/styles/game.scss b/client/styles/game.scss index 5c2a789..4eac8e3 100644 --- a/client/styles/game.scss +++ b/client/styles/game.scss @@ -14,6 +14,7 @@ body { display: flex; align-items: center; justify-content: center; + position: relative; } .roller-wrapper { diff --git a/client/wrappers/withSocket.jsx b/client/wrappers/withSocket.jsx new file mode 100644 index 0000000..fe68da9 --- /dev/null +++ b/client/wrappers/withSocket.jsx @@ -0,0 +1,22 @@ +import React, { Component } from 'react' +import socketIOClient from 'socket.io-client' + +let socket + +export default function withSocket(WrappedComponent) { + return class extends Component { + constructor(props) { + super(props) + + if (!socket) { + socket = socketIOClient('http://localhost:3000') + } + } + + render() { + return ( + + ) + } + } +} diff --git a/package.json b/package.json index cc7c77e..ae68750 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react-toastify": "^4.1.0", "redis": "^2.8.0", "serialport": "^6.2.1", + "shelljs": "^0.8.2", "slack-webhook": "^1.0.0", "socket.io": "^2.1.1", "socket.io-client": "^2.1.1" diff --git a/src/server.js b/src/server.js index ecd6486..2109def 100644 --- a/src/server.js +++ b/src/server.js @@ -6,7 +6,6 @@ import http from 'http' import reader, { parseData } from './rfid' import redis from 'redis' import moment from 'moment' -import servo from './servo' import sendPhoto from './queue' import Slack from './slack' import socketIO from 'socket.io' @@ -27,12 +26,13 @@ let readyForSpin = false let socketClient = null let user = null +import handleAdminPanelBroadcasts from './serverModules/handleAdminPanelBroadcasts' +import handleSpinResult from './serverModules/handleSpinResult' + app.use(webpackMiddleware(webpack(webpackConfig))) server.listen(3000) function _rollRequest(userId) { - user = getUser(userId) - if (user === undefined) { slack.log(userId) socketClient.emit('NOTIFY', 'error', 'please go to @czana') @@ -82,15 +82,20 @@ io.on('connection', client => { client.on('SPIN_ENDED', result => { readyForSpin = true - - if (result.win) { - if (result.cashPrize) servo.move() - slack.post(user.mention, result.icon, result.cashPrize ? '$$$' : '2 Kudos!') - } + handleSpinResult(result, user); }) + + handleAdminPanelBroadcasts(client); }) reader.on('data', data => { const userId = parseData(data) - _rollRequest(userId) + user = getUser(userId) + + if (user && user.admin) { + socketClient.emit('SET_ADMIN_PANEL_ACTIVE', true); + } else { + socketClient.emit('SET_ADMIN_PANEL_ACTIVE', false); + _rollRequest(userId) + } }) diff --git a/src/serverModules/handleAdminPanelBroadcasts.js b/src/serverModules/handleAdminPanelBroadcasts.js new file mode 100644 index 0000000..ea381a3 --- /dev/null +++ b/src/serverModules/handleAdminPanelBroadcasts.js @@ -0,0 +1,32 @@ +import shell from 'shelljs' + +import raspberryOnly from '../utils/raspberryOnly' + +let servo + +raspberryOnly(() => { + servo = require('../servo')['default'] +}) + +export default (client) => { + client.on('@admin/REBOOT', () => { + raspberryOnly(() => { + shell.exec('sudo reboot') + client.emit('SET_ADMIN_PANEL_ACTIVE', false); + }) + }) + + client.on('@admin/SHUTDOWN', () => { + raspberryOnly(() => { + shell.exec('sudo shutdown -P now') + client.emit('SET_ADMIN_PANEL_ACTIVE', false); + }) + }) + + client.on('@admin/TRIGGER_SERVO', () => { + raspberryOnly(() => { + servo.move() + client.emit('SET_ADMIN_PANEL_ACTIVE', false); + }) + }) +} diff --git a/src/serverModules/handleSpinResult.js b/src/serverModules/handleSpinResult.js new file mode 100644 index 0000000..17256dc --- /dev/null +++ b/src/serverModules/handleSpinResult.js @@ -0,0 +1,16 @@ +import raspberryOnly from '../utils/raspberryOnly' + +let servo + +raspberryOnly(() => { + servo = require('../servo') +}) + +export default (result, user) => { + if (result.win) { + raspberryOnly(() => { + if (result.cashPrize) { servo.move() } + }) + slack.post(user.mention, result.icon, result.cashPrize ? '$$$' : '2 Kudos!') + } +} diff --git a/src/users.js b/src/users.js index 6f07d23..0775785 100644 --- a/src/users.js +++ b/src/users.js @@ -2,6 +2,7 @@ const USERS = { 5501512322897: { mention: 'czana', email: 't.czana@selleo.com', + admin: true }, 4402038631408: { mention: 'bart', @@ -13,7 +14,8 @@ const USERS = { }, 4402038648119: { mention: 'm.polakowski', - email: 'm.polakowski@selleo.com' + email: 'm.polakowski@selleo.com', + admin: true }, 5501557468263: { mention: 'apawlicka', diff --git a/src/utils/raspberryOnly.js b/src/utils/raspberryOnly.js new file mode 100644 index 0000000..c635721 --- /dev/null +++ b/src/utils/raspberryOnly.js @@ -0,0 +1,7 @@ +require('dotenv').config() + +export default (theFunction) => { + if (process.env.RASPBERRY === '1') { + theFunction() + } +} diff --git a/webpack.config.js b/webpack.config.js index 3e367f8..a63d300 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,10 +1,16 @@ import HtmlWebpackPlugin from 'html-webpack-plugin' +import path from 'path' export default { mode: 'development', entry: './client/index.jsx', resolve: { - extensions: ['.js', '.jsx', '.scss'] + extensions: ['.js', '.jsx', '.scss'], + alias: { + '@components': path.resolve(__dirname, 'client/components'), + '@styles': path.resolve(__dirname, 'client/styles'), + '@wrappers': path.resolve(__dirname, 'client/wrappers') + } }, output: { path: '/', diff --git a/yarn.lock b/yarn.lock index be134c8..0f6c26e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3344,6 +3344,10 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -5517,6 +5521,12 @@ readline-utils@^2.2.1, readline-utils@^2.2.3: strip-color "^0.1.0" window-size "^1.1.0" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -5980,6 +5990,14 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +shelljs@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"