Implemented basic sup text support
This commit is contained in:
parent
c3e93ac603
commit
0d4fa14fec
35 changed files with 20292 additions and 3 deletions
7
.babelrc
Normal file
7
.babelrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"presets": ["es2015-node5"],
|
||||||
|
"plugins": [
|
||||||
|
"transform-async-to-generator",
|
||||||
|
"syntax-async-functions"
|
||||||
|
]
|
||||||
|
}
|
33
.eslintrc
Normal file
33
.eslintrc
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"extends": "airbnb/base",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": false
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"mocha"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"mocha/no-exclusive-tests": 2,
|
||||||
|
"semi": [2, "never"],
|
||||||
|
"max-len": [1, 120],
|
||||||
|
"prefer-const": 0,
|
||||||
|
"consistent-return": 0,
|
||||||
|
"no-param-reassign": [2, {"props": false}],
|
||||||
|
"no-use-before-define": [2, {"functions": false, "classes": true}],
|
||||||
|
"no-unused-vars": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"args": "none"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"describe": false,
|
||||||
|
"it": false,
|
||||||
|
"before": false,
|
||||||
|
"beforeEach": false,
|
||||||
|
"after": false,
|
||||||
|
"afterEach": false
|
||||||
|
}
|
||||||
|
}
|
41
app/client.js
Normal file
41
app/client.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
const socket = require('./socket')
|
||||||
|
|
||||||
|
socket.on('client.display', (data) => {
|
||||||
|
let exists = document.getElementById(data.key)
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
exists.tag.remove()
|
||||||
|
exists.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
let element = document.createElement('div')
|
||||||
|
element.innerHTML = data.html
|
||||||
|
element.id = data.key
|
||||||
|
element.classList.add('root-element')
|
||||||
|
|
||||||
|
let styleElement = document.createElement('style')
|
||||||
|
styleElement.setAttribute('type', 'text/css')
|
||||||
|
styleElement.innerHTML = data.css
|
||||||
|
|
||||||
|
element.tag = styleElement
|
||||||
|
|
||||||
|
document.body.appendChild(element)
|
||||||
|
document.head.appendChild(styleElement)
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
element.classList.add('root-element-display')
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('client.hide', (data) => {
|
||||||
|
let exists = document.getElementById(data.key)
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
exists.classList.remove('root-element-display')
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
exists.tag.remove()
|
||||||
|
exists.remove()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
})
|
98
app/controller/content.js
Normal file
98
app/controller/content.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const socket = require('../socket')
|
||||||
|
const store = require('./store')
|
||||||
|
|
||||||
|
const Content = { }
|
||||||
|
|
||||||
|
Content.vm = (function() {
|
||||||
|
let vm = {}
|
||||||
|
|
||||||
|
vm.storeUpdated = function() {
|
||||||
|
vm.content = store.get('content') || {}
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.init = function() {
|
||||||
|
vm.content = store.get('content') || {}
|
||||||
|
store.listen('content', vm.storeUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.onunload = function() {
|
||||||
|
store.unlisten('content')
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.updated = function(name, control) {
|
||||||
|
vm.content[name] = control.target.value
|
||||||
|
store.set('content', vm.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.display = function() {
|
||||||
|
socket.emit('content.display', vm.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.hide = function() {
|
||||||
|
socket.emit('content.hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm
|
||||||
|
})()
|
||||||
|
|
||||||
|
Content.controller = function() {
|
||||||
|
Content.vm.init()
|
||||||
|
|
||||||
|
this.onunload = Content.vm.onunload
|
||||||
|
}
|
||||||
|
|
||||||
|
Content.view = function() {
|
||||||
|
return m('div', [
|
||||||
|
m('h3', 'Content'),
|
||||||
|
m('div', { class: 'row' }, [
|
||||||
|
m('div', { class: 'small-12 columns' }, [
|
||||||
|
m('label', [
|
||||||
|
'HTML (use <%- name %> and <%- title %> for values)',
|
||||||
|
m('textarea', {
|
||||||
|
rows: '4',
|
||||||
|
oninput: Content.vm.updated.bind(null, 'html'),
|
||||||
|
value: Content.vm.content.html || '',
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('div', { class: 'small-12 columns' }, [
|
||||||
|
m('label', [
|
||||||
|
'CSS',
|
||||||
|
m('textarea', {
|
||||||
|
rows: '4',
|
||||||
|
oninput: Content.vm.updated.bind(null, 'css'),
|
||||||
|
value: Content.vm.content.css || '',
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('div', { class: 'small-12 columns' }, [
|
||||||
|
m('label', [
|
||||||
|
'Name',
|
||||||
|
m('input[type=text]', {
|
||||||
|
oninput: Content.vm.updated.bind(null, 'name'),
|
||||||
|
value: Content.vm.content.name || '',
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('div', { class: 'small-12 columns' }, [
|
||||||
|
m('label', [
|
||||||
|
'Title',
|
||||||
|
m('input[type=text]', {
|
||||||
|
oninput: Content.vm.updated.bind(null, 'title'),
|
||||||
|
value: Content.vm.content.title || '',
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('a.button', {
|
||||||
|
onclick: Content.vm.display
|
||||||
|
}, 'Display'),
|
||||||
|
m('a.button.alert', {
|
||||||
|
onclick: Content.vm.hide
|
||||||
|
}, 'Hide'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Content
|
23
app/controller/menu.js
Normal file
23
app/controller/menu.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
|
||||||
|
const Menu = {
|
||||||
|
controller: function() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(ctrl) {
|
||||||
|
return m('div', [
|
||||||
|
m('h3', 'Menu'),
|
||||||
|
m('ul', [
|
||||||
|
m('li', [
|
||||||
|
m('a', { href: '/', config: m.route }, 'Home'),
|
||||||
|
]),
|
||||||
|
m('li', [
|
||||||
|
m('a', { href: '/content', config: m.route }, 'Content'),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Menu
|
39
app/controller/store.js
Normal file
39
app/controller/store.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const socket = require('../socket')
|
||||||
|
const storage = {}
|
||||||
|
const events = {}
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
get: function(name) {
|
||||||
|
return storage[name]
|
||||||
|
},
|
||||||
|
|
||||||
|
set: function(name, value, dontSend) {
|
||||||
|
storage[name] = value
|
||||||
|
|
||||||
|
if (dontSend) {
|
||||||
|
if (events[name]) {
|
||||||
|
events[name]()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('store', {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
listen: function(name, caller) {
|
||||||
|
events[name] = caller
|
||||||
|
},
|
||||||
|
|
||||||
|
unlisten: function(name) {
|
||||||
|
delete events[name]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('store', (data) => {
|
||||||
|
store.set(data.name, data.value, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = store
|
27
app/main.js
Normal file
27
app/main.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* caspar-sup <https://filadelfia.is>
|
||||||
|
* Copyright 2015 Jonatan Nilsson <http://jonatan.nilsson.is/>
|
||||||
|
*
|
||||||
|
* Available under WTFPL License (http://www.wtfpl.net/txt/copying/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
//Add debug components to window. Allows us to play with controls
|
||||||
|
//in the console.
|
||||||
|
window.components = {}
|
||||||
|
|
||||||
|
require('./socket')
|
||||||
|
require('./controller/store')
|
||||||
|
|
||||||
|
const m = require('mithril')
|
||||||
|
const Menu = require('./controller/menu')
|
||||||
|
const Content = require('./controller/content')
|
||||||
|
|
||||||
|
m.mount(document.getElementById('menu'), Menu)
|
||||||
|
|
||||||
|
m.route(document.getElementById('content'), '/', {
|
||||||
|
'/': {},
|
||||||
|
'/content': Content,
|
||||||
|
});
|
5
app/socket.js
Normal file
5
app/socket.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
const io = require('socket.io-client')
|
||||||
|
|
||||||
|
const socket = io()
|
||||||
|
|
||||||
|
module.exports = socket
|
60
config.js
Normal file
60
config.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import nconf from 'nconf'
|
||||||
|
const pckg = require('./package.json')
|
||||||
|
|
||||||
|
// Helper method for global usage.
|
||||||
|
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||||
|
|
||||||
|
// Config follow the following priority check order:
|
||||||
|
// 1. Arguments
|
||||||
|
// 2. package.json
|
||||||
|
// 3. Enviroment variables
|
||||||
|
// 4. config/config.json
|
||||||
|
// 5. default settings
|
||||||
|
|
||||||
|
|
||||||
|
// Load arguments as highest priority
|
||||||
|
nconf.argv()
|
||||||
|
|
||||||
|
|
||||||
|
// Load package.json for name and such
|
||||||
|
let project = _.pick(pckg, ['name', 'version', 'description', 'author', 'license', 'homepage'])
|
||||||
|
|
||||||
|
|
||||||
|
// If we have global.it, there's a huge chance
|
||||||
|
// we're in test mode so we force node_env to be test.
|
||||||
|
if (typeof global.it === 'function') {
|
||||||
|
project.NODE_ENV = 'test'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Load overrides as second priority
|
||||||
|
nconf.overrides(project)
|
||||||
|
|
||||||
|
|
||||||
|
// Load enviroment variables as third priority
|
||||||
|
nconf.env()
|
||||||
|
|
||||||
|
|
||||||
|
// Load any overrides from the appropriate config file
|
||||||
|
let configFile = 'config/config.json'
|
||||||
|
|
||||||
|
if (nconf.get('NODE_ENV') === 'test') {
|
||||||
|
configFile = 'config/config.test.json'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
nconf.file('main', configFile)
|
||||||
|
|
||||||
|
// Load defaults
|
||||||
|
nconf.file('default', 'config/config.default.json')
|
||||||
|
|
||||||
|
|
||||||
|
// Final sanity checks
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (typeof global.it === 'function' & !nconf.inTest()) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Critical: potentially running test on production enviroment. Shutting down.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
module.exports = nconf
|
23
config/config.default.json
Normal file
23
config/config.default.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"NODE_ENV": "development",
|
||||||
|
"server": {
|
||||||
|
"port": 3000,
|
||||||
|
"host": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"bunyan": {
|
||||||
|
"name": "keywe",
|
||||||
|
"streams": [{
|
||||||
|
"stream": "process.stdout",
|
||||||
|
"level": "debug"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"knex": {
|
||||||
|
"client": "sqlite3",
|
||||||
|
"connection": {
|
||||||
|
"filename" : "./db.sqlite"
|
||||||
|
},
|
||||||
|
"migrations": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
config/config.json
Normal file
1
config/config.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
BIN
db.sqlite
Normal file
BIN
db.sqlite
Normal file
Binary file not shown.
38
index.js
Normal file
38
index.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
'use strict'
|
||||||
|
require('babel-register')
|
||||||
|
|
||||||
|
let log = require('./log').default
|
||||||
|
|
||||||
|
function exitHandler(options, err) {
|
||||||
|
if (options.cleanup) {
|
||||||
|
log.warn('Application is shutting down')
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
log.error('An unhandled error occured')
|
||||||
|
log.error(err)
|
||||||
|
}
|
||||||
|
if (options.exit) {
|
||||||
|
log.warn('Application is exiting')
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do something when app is closing
|
||||||
|
process.on('exit', exitHandler.bind(null, { cleanup: true }))
|
||||||
|
|
||||||
|
// catches ctrl+c event
|
||||||
|
process.on('SIGINT', exitHandler.bind(null, { exit: true }))
|
||||||
|
|
||||||
|
// catches uncaught exceptions
|
||||||
|
process.on('uncaughtException', exitHandler.bind(null, { exit: true }))
|
||||||
|
|
||||||
|
// Run the database script automatically.
|
||||||
|
log.info('Running database integrity scan.')
|
||||||
|
let setup = require('./script/setup')
|
||||||
|
|
||||||
|
setup().then(() => {
|
||||||
|
require('./server')
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error(error, 'Error while preparing database')
|
||||||
|
process.exit(1)
|
||||||
|
})
|
15
knexfile.js
Normal file
15
knexfile.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict'
|
||||||
|
require('babel-register')
|
||||||
|
|
||||||
|
const _ = require('lodash')
|
||||||
|
const config = require('./config')
|
||||||
|
|
||||||
|
let out = {}
|
||||||
|
|
||||||
|
// This is important for setup to run cleanly.
|
||||||
|
let knexConfig = _.cloneDeep(config.get('knex'))
|
||||||
|
knexConfig.pool = { min: 1, max: 1 }
|
||||||
|
|
||||||
|
out[config.get('NODE_ENV')] = knexConfig
|
||||||
|
|
||||||
|
module.exports = out
|
20
log.js
Normal file
20
log.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import bunyan from 'bunyan'
|
||||||
|
import config from './config'
|
||||||
|
|
||||||
|
// Clone the settings as we will be touching
|
||||||
|
// on them slightly.
|
||||||
|
let settings = _.cloneDeep(config.get('bunyan'))
|
||||||
|
|
||||||
|
// Replace any instance of 'process.stdout' with the
|
||||||
|
// actual reference to the process.stdout.
|
||||||
|
for (let i = 0; i < settings.streams.length; i++) {
|
||||||
|
if (settings.streams[i].stream === 'process.stdout') {
|
||||||
|
settings.streams[i].stream = process.stdout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our logger.
|
||||||
|
const logger = bunyan.createLogger(settings)
|
||||||
|
|
||||||
|
export default logger
|
23
migrations/20160410033515_base.js
Normal file
23
migrations/20160410033515_base.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function(knex, Promise) {
|
||||||
|
return Promise.all([
|
||||||
|
knex.schema.createTable('store', function(table) {
|
||||||
|
table.increments()
|
||||||
|
table.text('name')
|
||||||
|
table.text('value')
|
||||||
|
}).then(() => {
|
||||||
|
return knex('store').insert({
|
||||||
|
name: 'content',
|
||||||
|
value: '{}'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(knex, Promise) {
|
||||||
|
return Promise.all([
|
||||||
|
knex.schema.dropTable('store'),
|
||||||
|
]);
|
||||||
|
};
|
3
nodemon.json
Normal file
3
nodemon.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"ignore": ["app/*", "public/*"]
|
||||||
|
}
|
47
package.json
47
package.json
|
@ -1,10 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "caspar-sup",
|
"name": "caspar-sup",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "CasparCG superimposed graphics project",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm test"
|
"test": "npm test",
|
||||||
|
"build-main:js": "browserify app/main.js -o public/main.js --debug",
|
||||||
|
"watch-main:js": "watchify app/main.js -o public/main.js --debug",
|
||||||
|
"build-client:js": "browserify app/client.js -o public/client.js --debug",
|
||||||
|
"watch-client:js": "watchify app/client.js -o public/client.js --debug",
|
||||||
|
"build": "npm run build-main:js && npm run build-client:js",
|
||||||
|
"build:watch": "parallelshell \"npm run watch-main:js\" \"npm run watch-client:js\"",
|
||||||
|
"start": "node index.js",
|
||||||
|
"start:dev": "nodemon index.js | bunyan"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -20,5 +28,38 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/nfp-projects/caspar-sup/issues"
|
"url": "https://github.com/nfp-projects/caspar-sup/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/nfp-projects/caspar-sup#readme"
|
"homepage": "https://github.com/nfp-projects/caspar-sup#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"app-root-path": "^1.0.0",
|
||||||
|
"babel-plugin-syntax-async-functions": "^6.5.0",
|
||||||
|
"babel-plugin-transform-async-to-generator": "^6.7.0",
|
||||||
|
"babel-preset-es2015-node5": "^1.1.2",
|
||||||
|
"babel-register": "^6.7.2",
|
||||||
|
"bookshelf": "^0.9.2",
|
||||||
|
"browserify": "^13.0.0",
|
||||||
|
"bunyan": "^1.7.1",
|
||||||
|
"knex": "^0.10.0",
|
||||||
|
"koa": "^2.0.0-alpha.3",
|
||||||
|
"koa-socket": "^4.3.0",
|
||||||
|
"koa-static": "^3.0.0",
|
||||||
|
"lodash": "^4.6.1",
|
||||||
|
"mithril": "^0.2.3",
|
||||||
|
"nconf": "^0.8.4",
|
||||||
|
"parallelshell": "^2.0.0",
|
||||||
|
"socket.io": "^1.4.5",
|
||||||
|
"socket.io-client": "^1.4.5",
|
||||||
|
"sqlite3": "^3.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"assert-extended": "^1.0.1",
|
||||||
|
"babel-eslint": "^5.0.0",
|
||||||
|
"eslint": "^2.2.0",
|
||||||
|
"eslint-config-airbnb": "^6.1.0",
|
||||||
|
"eslint-plugin-mocha": "^2.0.0",
|
||||||
|
"live-reload": "^1.1.0",
|
||||||
|
"mocha": "^2.4.5",
|
||||||
|
"sinon": "^1.17.3",
|
||||||
|
"sinon-as-promised": "^4.0.0",
|
||||||
|
"watchify": "^3.7.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
9
public/client.css
Normal file
9
public/client.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.root-element {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-element-display {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
10
public/client.html
Normal file
10
public/client.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>CasparCG Client</title>
|
||||||
|
<link href="client.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="client.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7309
public/client.js
Normal file
7309
public/client.js
Normal file
File diff suppressed because one or more lines are too long
2555
public/foundation.css
vendored
Normal file
2555
public/foundation.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
15
public/index.html
Normal file
15
public/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>CasparCG Controller</title>
|
||||||
|
<link href="foundation.css" rel="stylesheet" />
|
||||||
|
<link href="main.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="row">
|
||||||
|
<div class="small-3 columns" id="menu"></div>
|
||||||
|
<div class="small-9 columns" id="content"></div>
|
||||||
|
</div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
public/main.css
Normal file
4
public/main.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
a.button {
|
||||||
|
margin: 0 1rem;
|
||||||
|
width: 10rem;
|
||||||
|
}
|
9604
public/main.js
Normal file
9604
public/main.js
Normal file
File diff suppressed because one or more lines are too long
45
script/setup.js
Normal file
45
script/setup.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const _ = require('lodash')
|
||||||
|
const appRoot = require('app-root-path')
|
||||||
|
const config = require(appRoot.resolve('/config'))
|
||||||
|
let log = require(appRoot.resolve('/log')).default
|
||||||
|
|
||||||
|
// This is important for setup to run cleanly.
|
||||||
|
let knexConfig = _.cloneDeep(config.get('knex'))
|
||||||
|
knexConfig.pool = { min: 1, max: 1 }
|
||||||
|
|
||||||
|
let knex = require('knex')(knexConfig)
|
||||||
|
|
||||||
|
log.info(knexConfig, 'Connected to database')
|
||||||
|
|
||||||
|
let setup = module.exports = () =>
|
||||||
|
knex.migrate.latest({
|
||||||
|
directory: appRoot.resolve('/migrations'),
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (result[1].length === 0) {
|
||||||
|
return log.info('Database is up to date')
|
||||||
|
}
|
||||||
|
for (let i = 0; i < result[1].length; i++) {
|
||||||
|
log.info('Applied migration from', result[1][i].substr(result[1][i].lastIndexOf('\\') + 1))
|
||||||
|
}
|
||||||
|
return knex.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
// Since we're running this as a script, we should output
|
||||||
|
// directly to the console.
|
||||||
|
log = console
|
||||||
|
log.info = console.log.bind(console)
|
||||||
|
|
||||||
|
setup().then(() => {
|
||||||
|
log.info('Setup ran successfully.')
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error(error, 'Error while running setup.')
|
||||||
|
}).then(() => {
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
}
|
71
server/bookshelf.js
Normal file
71
server/bookshelf.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import knex from 'knex'
|
||||||
|
import bookshelf from 'bookshelf'
|
||||||
|
|
||||||
|
import config from '../config'
|
||||||
|
import log from '../log'
|
||||||
|
|
||||||
|
let host = config.get('knex:connection')
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (host.match && host.match(/@[^/]+/)) {
|
||||||
|
host = host.match(/@[^/]+/)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(host, 'Connecting to DB')
|
||||||
|
|
||||||
|
const client = knex(config.get('knex'))
|
||||||
|
|
||||||
|
// Check if we're running tests while connected to
|
||||||
|
// potential production environment.
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (config.get('NODE_ENV') === 'test' &&
|
||||||
|
config.get('knex:connection:database') !== 'test' ||
|
||||||
|
config.get('knex:connection:connection')) {
|
||||||
|
// There is an offchance that we're running tests on
|
||||||
|
// production database. Exit NOW!
|
||||||
|
log.error('Critical: potentially running test on production enviroment. Shutting down.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let shelf = bookshelf(client)
|
||||||
|
|
||||||
|
// Helper method to create models
|
||||||
|
shelf.createModel = (attr, opts) => {
|
||||||
|
// Create default attributes to all models
|
||||||
|
let attributes = _.defaults(attr, {
|
||||||
|
initialize() {
|
||||||
|
this.on('fetching', this.checkFetching)
|
||||||
|
},
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
return this.destroy()
|
||||||
|
},
|
||||||
|
|
||||||
|
checkFetching(model, columns, options) {
|
||||||
|
// options.query.where({ is_deleted: false })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create default options for all models
|
||||||
|
let options = _.defaults(opts, {
|
||||||
|
create(data) {
|
||||||
|
return this.forge(data).save()
|
||||||
|
},
|
||||||
|
|
||||||
|
getSingle(id, withRelated = [], required = true) {
|
||||||
|
let where = { id: Number(id) || 0 }
|
||||||
|
|
||||||
|
return this.query({ where })
|
||||||
|
.fetch({ require, withRelated })
|
||||||
|
},
|
||||||
|
|
||||||
|
getAll(where = {}, withRelated = []) {
|
||||||
|
return this.query({ where })
|
||||||
|
.fetchAll({ withRelated })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return shelf.Model.extend(attributes, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default shelf
|
1
server/index.js
Normal file
1
server/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
import './koa'
|
34
server/io/connection.js
Normal file
34
server/io/connection.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import Store from './store/model'
|
||||||
|
|
||||||
|
export function register(ctx, name, method) {
|
||||||
|
if (_.isPlainObject(method)) {
|
||||||
|
Object.keys(method).forEach(key => {
|
||||||
|
register(ctx, [name, key].join('.'), method[key])
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.socket.on(name, async (data) => {
|
||||||
|
if (name !== 'store') {
|
||||||
|
ctx.log.info(`Got event ${name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await method(ctx, data)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ctx.log.error(error, `Error processing ${name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function newConnection(ctx) {
|
||||||
|
ctx.log.info('Got new socket connection')
|
||||||
|
|
||||||
|
let data = await Store.getAll()
|
||||||
|
|
||||||
|
data.forEach(item =>
|
||||||
|
ctx.socket.emit('store', item.toJSON())
|
||||||
|
)
|
||||||
|
}
|
18
server/io/content/routes.js
Normal file
18
server/io/content/routes.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export function display(ctx, data) {
|
||||||
|
let compiled = _.template(data.html)
|
||||||
|
let html = compiled(data)
|
||||||
|
|
||||||
|
ctx.io.emit('client.display', {
|
||||||
|
key: 'content',
|
||||||
|
html,
|
||||||
|
css: data.css,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide(ctx) {
|
||||||
|
ctx.io.emit('client.hide', {
|
||||||
|
key: 'content',
|
||||||
|
})
|
||||||
|
}
|
22
server/io/router.js
Normal file
22
server/io/router.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import logger from '../../log'
|
||||||
|
import { register, newConnection } from './connection'
|
||||||
|
|
||||||
|
import * as content from './content/routes'
|
||||||
|
import * as store from './store/routes'
|
||||||
|
|
||||||
|
function onConnection(server, data) {
|
||||||
|
const io = server.socket
|
||||||
|
const socket = data.socket
|
||||||
|
const log = logger.child({
|
||||||
|
id: socket.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
let ctx = { io, socket, log }
|
||||||
|
|
||||||
|
newConnection(ctx)
|
||||||
|
|
||||||
|
register(ctx, 'content', content)
|
||||||
|
register(ctx, 'store', store.updateStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default onConnection
|
24
server/io/store/model.js
Normal file
24
server/io/store/model.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import bookshelf from '../../bookshelf'
|
||||||
|
|
||||||
|
const Store = bookshelf.createModel({
|
||||||
|
tableName: 'store',
|
||||||
|
|
||||||
|
format(attributes) {
|
||||||
|
attributes.value = JSON.stringify(attributes.value)
|
||||||
|
return attributes
|
||||||
|
},
|
||||||
|
|
||||||
|
parse(attributes) {
|
||||||
|
attributes.value = JSON.parse(attributes.value)
|
||||||
|
return attributes
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
getSingle(name, withRelated = [], required = true) {
|
||||||
|
let where = { name }
|
||||||
|
|
||||||
|
return this.query({ where })
|
||||||
|
.fetch({ require, withRelated })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Store
|
11
server/io/store/routes.js
Normal file
11
server/io/store/routes.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import Store from './model'
|
||||||
|
|
||||||
|
export async function updateStore(ctx, data) {
|
||||||
|
let item = await Store.getSingle(data.name)
|
||||||
|
|
||||||
|
item.set('value', data.value)
|
||||||
|
|
||||||
|
await item.save()
|
||||||
|
|
||||||
|
ctx.socket.broadcast.emit('store', item.toJSON())
|
||||||
|
}
|
20
server/koa.js
Normal file
20
server/koa.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import serve from 'koa-static'
|
||||||
|
import Koa from 'koa'
|
||||||
|
import socket from 'koa-socket'
|
||||||
|
import config from '../config'
|
||||||
|
import log from '../log'
|
||||||
|
import router from './io/router'
|
||||||
|
import { bunyanLogger, errorHandler } from './middlewares'
|
||||||
|
|
||||||
|
const app = new Koa()
|
||||||
|
const io = new socket()
|
||||||
|
|
||||||
|
io.attach(app)
|
||||||
|
|
||||||
|
io.on('connection', router.bind(this, io))
|
||||||
|
|
||||||
|
app.use(bunyanLogger(log))
|
||||||
|
app.use(errorHandler())
|
||||||
|
app.use(serve('public'))
|
||||||
|
|
||||||
|
app.listen(config.get('server:port'))
|
40
server/middlewares.js
Normal file
40
server/middlewares.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
export function bunyanLogger(logger) {
|
||||||
|
return async (ctx, next) => {
|
||||||
|
ctx.log = logger.child({
|
||||||
|
})
|
||||||
|
|
||||||
|
const d1 = new Date().getTime()
|
||||||
|
|
||||||
|
await next()
|
||||||
|
|
||||||
|
const d2 = new Date().getTime()
|
||||||
|
|
||||||
|
let level = 'info'
|
||||||
|
if (ctx.status >= 400) {
|
||||||
|
level = 'warn'
|
||||||
|
}
|
||||||
|
if (ctx.status >= 500) {
|
||||||
|
level = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.log[level]({
|
||||||
|
duration: (d2 - d1),
|
||||||
|
status: ctx.res.statusCode,
|
||||||
|
}, `<-- ${ctx.request.method} ${ctx.request.url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorHandler() {
|
||||||
|
return async (ctx, next) => {
|
||||||
|
try {
|
||||||
|
await next()
|
||||||
|
} catch(err) {
|
||||||
|
ctx.log.error(err, 'Unknown error occured')
|
||||||
|
ctx.status = 500
|
||||||
|
ctx.render('error', {
|
||||||
|
error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue