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"