├── public ├── javascripts │ ├── noop.js │ ├── common │ │ ├── string-utils.js │ │ ├── http-error.js │ │ ├── number-utils.js │ │ ├── ajax.js │ │ └── metered-request.js │ ├── services │ │ ├── index.js │ │ └── api-subscriptions.js │ ├── mixins │ │ ├── navigation-utils.js │ │ ├── store-change.js │ │ └── overlay.js │ ├── components │ │ ├── route-not-found.jsx │ │ ├── app.jsx │ │ ├── overlays │ │ │ ├── overlay-manager.jsx │ │ │ ├── alert.jsx │ │ │ ├── confirm.jsx │ │ │ └── item-form.jsx │ │ ├── nav-bar.jsx │ │ ├── server-time.jsx │ │ └── items.jsx │ ├── stores │ │ ├── index.js │ │ ├── items.js │ │ ├── overlays.js │ │ ├── server-time.js │ │ ├── base.js │ │ └── crud-base.js │ ├── routes.jsx │ ├── dispatcher.js │ ├── constants │ │ ├── states.js │ │ └── actions.js │ ├── actions │ │ ├── items.js │ │ ├── server-time.js │ │ ├── overlays.js │ │ ├── base.js │ │ └── crud-base.js │ ├── main.jsx │ └── vendor │ │ └── flux │ │ ├── invariant.js │ │ └── Dispatcher.js ├── images │ └── favicon.ico ├── fonts │ └── bootstrap │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ ├── glyphicons-halflings-regular.woff2 │ │ └── glyphicons-halflings-regular.svg └── less │ └── styles.less ├── views ├── error.ejs └── index.ejs ├── gulp ├── tasks │ ├── default.js │ ├── build.js │ ├── browserify-main.js │ ├── browserify-vendor.js │ ├── bootstrap.js │ ├── watch.js │ └── less.js ├── util │ ├── handleErrors.js │ ├── bundleLogger.js │ └── bundler.js └── config.js ├── .gitignore ├── gulpfile.js ├── routes ├── index.js └── api.js ├── LICENSE ├── README.md ├── app.js ├── package.json ├── bin └── www ├── .jshintrc └── pubsub-bridge.js /public/javascripts/noop.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/react-flux-starter/master/public/images/favicon.ico -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /gulp/tasks/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | gulp.task('default', ['watch']); 6 | -------------------------------------------------------------------------------- /gulp/tasks/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | gulp.task('build', ['main.js', 'vendor.js', 'less']); 6 | -------------------------------------------------------------------------------- /public/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/react-flux-starter/master/public/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/react-flux-starter/master/public/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/react-flux-starter/master/public/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/react-flux-starter/master/public/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/javascripts/common/string-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validator = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i; 4 | 5 | module.exports = { 6 | isValidEmail: function (s) { 7 | return validator.test(s); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /public/javascripts/services/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var apiSubscriptionSrvc = require('./api-subscriptions'); 4 | 5 | exports.initialize = function (accessToken) { 6 | exports.apiSubscriptions = apiSubscriptionSrvc(accessToken); 7 | }; 8 | -------------------------------------------------------------------------------- /public/less/styles.less: -------------------------------------------------------------------------------- 1 | // Define any overrides of bootstrap before importing 2 | // 3 | @icon-font-path: "../fonts/bootstrap/"; 4 | 5 | // include bootstrap 6 | @import 'node_modules/bootstrap/less/bootstrap.less'; 7 | 8 | // place your app CSS here or in a separate .less file 9 | -------------------------------------------------------------------------------- /gulp/tasks/browserify-main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var config = require('../config').browserify; 5 | var bundler = require('../util/bundler'); 6 | 7 | gulp.task('main.js', function(callback) { 8 | bundler(config.bundleConfigs.main) 9 | .on('end', callback); 10 | }); 11 | -------------------------------------------------------------------------------- /gulp/tasks/browserify-vendor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var config = require('../config').browserify; 5 | var bundler = require('../util/bundler'); 6 | 7 | gulp.task('vendor.js', function(callback) { 8 | bundler(config.bundleConfigs.vendor) 9 | .on('end', callback); 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mr Developer 2 | .DS_Store 3 | 4 | #Emacs cruft 5 | *~ 6 | 7 | # node modules 8 | node_modules/ 9 | 10 | # npm debug log 11 | npm-debug.log 12 | 13 | # app specific files that are generated as part of the asset pipeline 14 | #public/fonts/bootstrap 15 | #public/javascripts/bundles/ 16 | #public/stylesheets/ 17 | -------------------------------------------------------------------------------- /public/javascripts/mixins/navigation-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var History = require('react-router').History; 4 | 5 | module.exports = { 6 | 7 | backOrTransitionTo: function (routeName, params) { 8 | if (History.length > 1) { 9 | this.goBack(); 10 | } 11 | else { 12 | this.transitionTo(routeName, params); 13 | } 14 | } 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /gulp/util/handleErrors.js: -------------------------------------------------------------------------------- 1 | var notify = require("gulp-notify"); 2 | 3 | module.exports = function() { 4 | 5 | var args = Array.prototype.slice.call(arguments); 6 | 7 | // Send error to notification center with gulp-notify 8 | notify.onError({ 9 | title: "Compile Error", 10 | message: "<%= error.message %>" 11 | }).apply(this, args); 12 | 13 | // Keep gulp from hanging on this task 14 | this.emit('end'); 15 | }; 16 | -------------------------------------------------------------------------------- /public/javascripts/components/route-not-found.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | module.exports = React.createClass({ 6 | render: function () { 7 | return ( 8 |
9 |
10 |
11 | Route Not Found 12 |
13 |
14 |
15 | ); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /gulp/tasks/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | path = require('path'), 5 | config = require('../config').bootstrap; 6 | 7 | 8 | // copy bootstrap fonts into the public folder 9 | gulp.task('bootstrap', function () { 10 | 11 | /* BOOTSTRAP */ 12 | 13 | // copy over all the fonts 14 | gulp.src(path.join(config.bootstrapHome, 'fonts/**/*')) 15 | .pipe(gulp.dest(config.fonts)); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /public/javascripts/common/http-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class HTTPError extends Error { 4 | constructor(url, xhr, status, err, response) { 5 | this.url = url; 6 | this.xhr = xhr; 7 | this.status = status; 8 | this.error = err; 9 | this.response = response; 10 | } 11 | 12 | toString() { 13 | return `${this.constructor.name} (status=${this.xhr.status}, url=${this.url})`; 14 | } 15 | } 16 | 17 | module.exports = HTTPError; 18 | -------------------------------------------------------------------------------- /gulp/tasks/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Notes: ??? NEEDS TO BE UPDATED TO USE WATCHIFY 4 | - gulp/tasks/browserify.js handles js recompiling with watchify 5 | - gulp/tasks/browserSync.js watches and reloads compiled files 6 | */ 7 | 8 | var gulp = require('gulp'); 9 | var config = require('../config'); 10 | 11 | gulp.task('watch', ['build'], function() { 12 | gulp.watch(config.less.watch, ['less']); 13 | gulp.watch(config.browserify.bundleConfigs.main.watch, ['main.js']); 14 | }); 15 | -------------------------------------------------------------------------------- /public/javascripts/stores/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Dispatcher = require('../dispatcher'); 4 | 5 | var ItemsStore = require('./items'), 6 | ServerTimeStore = require('./server-time'), 7 | OverlaysStore = require('./overlays'); 8 | 9 | exports.initialize = function () { 10 | 11 | exports.ItemsStore = new ItemsStore(Dispatcher); 12 | exports.ServerTimeStore = new ServerTimeStore(Dispatcher); 13 | 14 | exports.OverlaysStore = new OverlaysStore(Dispatcher); 15 | 16 | return this; 17 | }; 18 | -------------------------------------------------------------------------------- /gulp/tasks/less.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | less = require('gulp-less'), 5 | autoprefixer = require('gulp-autoprefixer'), 6 | sourcemaps = require('gulp-sourcemaps'), 7 | handleErrors = require('../util/handleErrors'), 8 | config = require('../config').less; 9 | 10 | gulp.task('less', function() { 11 | return gulp.src(config.src) 12 | .pipe(sourcemaps.init()) 13 | .pipe(less()) 14 | .on('error', handleErrors) 15 | .pipe(autoprefixer({cascade: false, browsers: ['last 2 versions']})) 16 | .pipe(sourcemaps.write()) 17 | .pipe(gulp.dest(config.dest)); 18 | }); 19 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | gulpfile.js 3 | =========== 4 | Rather than manage one giant configuration file responsible 5 | for creating multiple tasks, each task has been broken out into 6 | its own file in gulp/tasks. Any files in that directory get 7 | automatically required below. 8 | To add a new task, simply add a new task file in that directory. 9 | gulp/tasks/default.js specifies the default set of tasks to run 10 | when you run `gulp`. 11 | */ 12 | 13 | var requireDir = require('require-dir'); 14 | 15 | // Require all tasks in gulp/tasks, including subfolders 16 | requireDir('./gulp/tasks', { recurse: true }); 17 | -------------------------------------------------------------------------------- /public/javascripts/routes.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Router = require('react-router'), 6 | Route = Router.Route, 7 | NotFoundRoute = Router.NotFoundRoute, 8 | DefaultRoute = Router.Route; 9 | 10 | module.exports = ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /gulp/util/bundleLogger.js: -------------------------------------------------------------------------------- 1 | /* bundleLogger 2 | ------------ 3 | Provides gulp style logs to the bundle method in browserify.js 4 | */ 5 | 6 | var gutil = require('gulp-util'); 7 | var prettyHrtime = require('pretty-hrtime'); 8 | var startTime; 9 | 10 | module.exports = { 11 | start: function(filepath) { 12 | startTime = process.hrtime(); 13 | gutil.log('Bundling', gutil.colors.green(filepath) + '...'); 14 | }, 15 | 16 | end: function(filepath) { 17 | var taskTime = process.hrtime(startTime); 18 | var prettyTime = prettyHrtime(taskTime); 19 | gutil.log('Bundled', gutil.colors.green(filepath), 'in', gutil.colors.magenta(prettyTime)); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /public/javascripts/components/app.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // bootstrap initialization 4 | var $ = require('jquery'); 5 | window.jQuery = $; 6 | require('bootstrap'); 7 | 8 | var React = require('react/addons'); 9 | 10 | var Router = require('react-router'), 11 | RouteHandler = Router.RouteHandler; 12 | 13 | var NavBar = require('./nav-bar.jsx'), 14 | OverlayManager = require('./overlays/overlay-manager.jsx'); 15 | 16 | module.exports = React.createClass({ 17 | 18 | mixins: [Router.State], 19 | 20 | render: function () { 21 | return ( 22 |
23 | 24 | 25 | 26 |
27 | ); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /public/javascripts/stores/items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var CRUDBase = require('./crud-base'); 6 | 7 | var ItemActions = require('../actions/items'); 8 | 9 | /** 10 | * Basic CRUD store for a RESTful JSON "resource". Overriding "getAll" to add 11 | * sort order to resources lists. 12 | */ 13 | class ItemsStore extends CRUDBase { 14 | 15 | // specify the action instance and action object identifier (and dispatcher) 16 | constructor(dispatcher) { 17 | super(dispatcher, ItemActions, 'ITEM'); 18 | } 19 | 20 | getAll() { 21 | return _.sortBy(super.getAll(), item => item.data.last + item.data.first); 22 | } 23 | 24 | } 25 | 26 | module.exports = ItemsStore; 27 | -------------------------------------------------------------------------------- /public/javascripts/common/number-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var re = /\B(?=(\d{3})+(?!\d))/g; 4 | 5 | module.exports = { 6 | numberWithCommas: function (x, defaultValue='-') { 7 | if (!x) { return defaultValue; } 8 | return x.toString().replace(re, ","); 9 | }, 10 | 11 | formatTimeForInterval: function (timestamp, interval="hour") { 12 | var result; 13 | switch (interval) { 14 | case "day": 15 | result = timestamp.format("ddd, MMM D, YYYY"); 16 | break; 17 | case "hour": 18 | result = timestamp.format("ddd, MMM D, YYYY, h:mm A"); 19 | break; 20 | default: 21 | // default is "minute" 22 | result = timestamp.format("ddd, MMM D, YYYY, h:mm:ss A"); 23 | } 24 | return result; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /public/javascripts/components/overlays/overlay-manager.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Stores = require('../../stores'), 6 | storeChangeMixin = require('../../mixins/store-change'); 7 | 8 | /** 9 | * Basic manager for displaying overlays 10 | * 11 | * Only 1 overlay at a time can be displayed. 12 | * 13 | * Overlays are responsible for their own popping 14 | * 15 | */ 16 | 17 | module.exports = React.createClass({ 18 | 19 | mixins: [storeChangeMixin(Stores.OverlaysStore)], 20 | 21 | storeChange: function () { 22 | this.forceUpdate(); 23 | }, 24 | 25 | render: function () { 26 | var overlay = Stores.OverlaysStore.getTopOverlay(); 27 | if (!overlay) { return null; } 28 | return overlay; 29 | } 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /public/javascripts/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Dispatcher = require('./vendor/flux/Dispatcher'); 5 | 6 | module.exports = _.extend(new Dispatcher(), { 7 | 8 | /** 9 | * A bridge function between the views and the dispatcher, marking the action 10 | * as a view action. 11 | * @param {object} action The data coming from the view. 12 | */ 13 | handleViewAction: function(action) { 14 | this.dispatch({ 15 | source: 'VIEW_ACTION', 16 | action: action 17 | }); 18 | }, 19 | 20 | /** 21 | * A bridge function between the server and the dispatcher, marking the action 22 | * as a server action. 23 | * @param {object} action The data coming from the view. 24 | */ 25 | handleServerAction: function(action) { 26 | this.dispatch({ 27 | source: 'SERVER_ACTION', 28 | action: action 29 | }); 30 | } 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /public/javascripts/common/ajax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper for $.ajax() that returns ES6 promises instead 3 | * of jQuery promises. 4 | * @module common/ajax 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var $ = require('jquery'); 10 | 11 | var HTTPError = require('./http-error'); 12 | 13 | 14 | module.exports = function(opts) { 15 | var promise = new Promise(function(resolve, reject) { 16 | $.ajax(opts) 17 | .done(function(data) { 18 | resolve(data); 19 | }) 20 | .fail(function(xhr, status, err) { 21 | var response; 22 | if (xhr.status === 0 && xhr.responseText === undefined) { 23 | response = {detail:'Possible CORS error; check your browser console for further details'}; 24 | } 25 | else { 26 | response = xhr.responseJSON; 27 | } 28 | 29 | reject(new HTTPError(opts.url, xhr, status, err, response)); 30 | }); 31 | }); 32 | 33 | return promise; 34 | }; 35 | -------------------------------------------------------------------------------- /public/javascripts/constants/states.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var keyMirror = require('react/lib/keyMirror'); 4 | 5 | /** 6 | * Store entities need the concept of "new", "dirty", "deleted" (i.e. isNew, isDirty, isDelete) which 7 | * when combined with state (synced, request, errored) provides components good detail on how to 8 | * render 9 | * 10 | */ 11 | 12 | module.exports = keyMirror({ 13 | 14 | /* entity states */ 15 | SYNCED: null, // entity is in sync with backend 16 | LOADING: null, // entity is in-process of being fetched from backend (implies GET) 17 | NEW: null, // entity is new and in-process of syncing with backend 18 | SAVING: null, // entity is dirty and in-process of syncing with backend 19 | DELETING: null, // entity has been deleted and in-process of syncing with backend 20 | ERRORED: null // entity is in an error state and potentially out-of-sync with server 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /public/javascripts/stores/overlays.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var kActions = require('../constants/actions'); 6 | 7 | var BaseStore = require('./base'); 8 | 9 | var _handlers = _.zipObject([ 10 | [kActions.OVERLAY_PUSH, '_onOverlayPush'], 11 | [kActions.OVERLAY_POP, '_onOverlayPop'] 12 | ]); 13 | 14 | 15 | class OverlaysStore extends BaseStore { 16 | 17 | initialize() { 18 | this._overlays = []; 19 | } 20 | 21 | _getActions(){ 22 | return _handlers; 23 | } 24 | 25 | getTopOverlay() { 26 | return this._overlays.length ? this._overlays[this._overlays.length - 1] : null; 27 | } 28 | 29 | // action handlers 30 | _onOverlayPush(payload) { 31 | this._overlays.push(payload.data.component); 32 | this.emitChange(); 33 | } 34 | 35 | _onOverlayPop() { 36 | this._overlays.pop(); 37 | this.emitChange(); 38 | } 39 | 40 | } 41 | 42 | module.exports = OverlaysStore; 43 | -------------------------------------------------------------------------------- /public/javascripts/actions/items.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CRUDBase = require('./crud-base'); 4 | 5 | /** 6 | * Basic CRUD actions for a RESTful JSON "resource". Overriding "post" and "put" 7 | * to create JSON payload that the endpoint expects. 8 | */ 9 | 10 | class ItemActions extends CRUDBase { 11 | 12 | // specify the baseURL and action object identifier for dispatches 13 | constructor() { 14 | super('/api/items', 'ITEM'); 15 | } 16 | 17 | // define "create" json payload appropriate for resource 18 | post (first, last) { 19 | var data = { 20 | first: first, 21 | last: last 22 | }; 23 | return super.post(data); 24 | } 25 | 26 | // define "update" json payload appropriate for resource 27 | put (id, first, last) { 28 | var data = { 29 | id: id, 30 | first: first, 31 | last: last 32 | }; 33 | super.put(id, data); 34 | } 35 | 36 | } 37 | 38 | module.exports = new ItemActions(); 39 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | 5 | var router = express.Router(); 6 | 7 | /** 8 | * Standard handler for returning index template 9 | * @function 10 | */ 11 | function indexRouteHandler (req, res) { 12 | res.render('index', { 13 | title: 'Example', 14 | token: req['heroku-bouncer'] && req['heroku-bouncer'].token || '', 15 | herokuId: req['heroku-bouncer'] && req['heroku-bouncer'].id || '' 16 | }); 17 | } 18 | 19 | router.get('/api/users/me', function (req, res) { 20 | res.json(req['heroku-bouncer']); 21 | }); 22 | 23 | router.get('/api/items', function (req, res) { 24 | res.json([ 25 | {id: 1, first: 'Howard', last: 'Burrows'}, 26 | {id: 2, first: 'David', last: 'Gouldin'}, 27 | {id: 3, first: 'Scott', last: 'Persinger'} 28 | ]); 29 | }); 30 | 31 | router.get('/api/servertime', function (req, res) { 32 | res.json(new Date().toString()); 33 | }); 34 | 35 | /* GET home page. */ 36 | router.get('/*', indexRouteHandler); 37 | 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /public/javascripts/mixins/store-change.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var StoreChangeMixin = function() { 6 | var args = Array.prototype.slice.call(arguments); 7 | 8 | return { 9 | 10 | componentWillMount: function() { 11 | if (!_.isFunction(this.storeChange)) { 12 | throw new Error('StoreChangeMixin requires storeChange handler'); 13 | } 14 | 15 | _.each(args, function(store) { 16 | store.addChangeListener(this.storeChange); 17 | }, this); 18 | }, 19 | 20 | componentWillUnmount: function() { 21 | _.each(args, function(store) { 22 | store.removeChangeListener(this.storeChange); 23 | }, this); 24 | } 25 | 26 | }; 27 | }; 28 | 29 | StoreChangeMixin.componentWillMount = function() { 30 | throw new Error("StoreChangeMixin is a function that takes one or more " + 31 | "store names as parameters and returns the mixin, e.g.: " + 32 | "mixins[StoreChangeMixin(store1, store2)]"); 33 | }; 34 | 35 | module.exports = StoreChangeMixin; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Copyright (C) 2015 Heroku, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /public/javascripts/main.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | /** 6 | * Main entry-point 7 | */ 8 | $(function () { 9 | 10 | var React = require('react/addons'); 11 | 12 | var Router = require('react-router'); 13 | 14 | // for developer tools 15 | window.React = React; 16 | 17 | React.initializeTouchEvents(true); 18 | 19 | // services initialization 20 | var Services = require('./services'); 21 | Services.initialize(window.EX.const.apiAccessToken); 22 | 23 | // store initialization -- needs to be done before any component references 24 | var Stores = require('./stores'); 25 | Stores.initialize(); 26 | 27 | // for debugging - allows you to query the stores from the browser console 28 | window._stores = Stores; 29 | 30 | var Routes = require('./routes.jsx'); 31 | 32 | var router = Router.create({ 33 | routes: Routes, 34 | location: Router.HistoryLocation, 35 | onError: function () { 36 | alert('unexpected error in Router'); 37 | } 38 | }); 39 | 40 | router.run(function (Handler) { 41 | React.render(, document.body); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-flux-starter 2 | 3 | An application template for a single page webapp backed by Express that uses React with a Flux architecture with support for Actions, Stores, message dispatching, and real-time updates using web sockets. Uses react-router for page routing. 4 | 5 | Uses Bootstrap and LESS for CSS, Browserify for Javascript bundling, ES6 syntax via Traceur, and Gulp for managing all the front-end asset build and packaging. 6 | 7 | To use: 8 | 9 | Clone this repository the execute the following commands: 10 | 11 | 12 | 1. `npm install` 13 | 1. `./node_modules/gulp/bin/gulp.js bootstrap` 14 | 1. `./node_modules/gulp/bin/gulp.js build` 15 | 1. `npm start` 16 | 1. Goto http://localhost:3000 in your browser 17 | 18 | If you want to develop further and make modifications: 19 | 20 | 21 | 1. In one console window run `./node_modules/gulp/bin/gulp.js`. This will run Gulp in "watch" mode and automatially rebuild your frontend assets on change. 22 | 1. In a second console window run `npm start`. If you're going to modify the Express code use nodemon (`nodemon ./bin/www`) which will automatically reload Express after you make a change. 23 | 24 | Enjoy!! 25 | -------------------------------------------------------------------------------- /public/javascripts/stores/server-time.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var BaseStore = require('./base'); 6 | 7 | var kActions = require('../constants/actions'), 8 | ServerActions = require('../actions/server-time'); 9 | 10 | var _actions = _.zipObject([ 11 | [kActions.SERVERTIME_GET, 'handleSet'], 12 | [kActions.SERVERTIME_PUT, 'handleSet'] 13 | ]); 14 | 15 | class ServerTimeStore extends BaseStore { 16 | 17 | constructor(dispatcher) { 18 | super(dispatcher); 19 | this._serverTime = undefined; 20 | } 21 | 22 | _getActions() { 23 | return _actions; 24 | } 25 | 26 | _load() { 27 | ServerActions.getTime(); 28 | return undefined; 29 | } 30 | 31 | _getTime() { 32 | return this._serverTime !== undefined ? this._serverTime : this._load(); 33 | } 34 | 35 | getServerTime() { 36 | return this._getTime(); 37 | } 38 | 39 | 40 | /* 41 | * 42 | * Action Handlers 43 | * 44 | */ 45 | 46 | handleSet(payload) { 47 | console.debug(`${this.getStoreName()}:handleSet state=${payload.syncState}`); 48 | this._serverTime = this.makeStatefulEntry(payload.syncState, payload.data); 49 | this.emitChange(); 50 | } 51 | 52 | } 53 | 54 | module.exports = ServerTimeStore; 55 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= title %> 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/javascripts/mixins/overlay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | var Overlays = require('../actions/overlays'); 6 | 7 | 8 | module.exports = { 9 | 10 | componentDidMount: function() { 11 | var $node = $(this.getDOMNode()); 12 | 13 | $node.on('shown.bs.modal', this._handleModalShown); 14 | $node.on('hidden.bs.modal', this._handleModalHidden); 15 | $(document).on('keyup', this._handleKeyUp); 16 | 17 | $node.modal({backdrop: 'static', keyboard: true}); 18 | }, 19 | 20 | componentWillUnmount: function() { 21 | var $node = $(this.getDOMNode()); 22 | $node.off('hidden.bs.modal', this._handleModalHidden); 23 | $node.off('hidden.bs.modal', this._handleModalShown); 24 | $(document).off('keyup', this._handleKeyUp); 25 | }, 26 | 27 | hideModal: function () { 28 | $(this.getDOMNode()).modal('hide'); 29 | }, 30 | 31 | _handleModalShown: function () { 32 | if (this.handleModalShown) { 33 | this.handleModalShown(); 34 | } 35 | }, 36 | 37 | _handleModalHidden: function() { 38 | if (this.handleModalHidden) { 39 | this.handleModalHidden(); 40 | } 41 | Overlays.pop(); 42 | }, 43 | 44 | _handleKeyUp: function (e) { 45 | if (e.keyCode === 27) { 46 | this.hideModal(); 47 | } 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /public/javascripts/constants/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var keyMirror = require('react/lib/keyMirror'); 4 | 5 | /** 6 | * Action type constants. Should follow the format: 7 | * _ 8 | * 9 | * For example, an action for fetching a specific "Item" object: 10 | * ITEM_GET 11 | * 12 | * If you're using the CRUD Action and Store base classes the verbs must be the following: 13 | * GETALL <- Retrieving a list of objects. (e.g. GET /items) 14 | * GETONE <- Get a single object (e.g. GET /items/:id) 15 | * POST <- Creating an object. (e.g. POST /items) 16 | * PUT <- Update an existing object. (e.g. PUT /items/:id) 17 | * DELETE <- Deleting an object. (e.g. DELETE /items/:id) 18 | * 19 | * Some actions types may not have a receiver, which is OK. The result of POST, PUT, and DELETE actions 20 | * may enter back into the system through subscriptions rather than in response to API requests. 21 | */ 22 | 23 | module.exports = keyMirror({ 24 | 25 | // item actions 26 | ITEM_GETALL: null, 27 | ITEM_GETONE: null, 28 | ITEM_POST: null, 29 | ITEM_PUT: null, 30 | ITEM_DELETE: null, 31 | 32 | // servertime actions 33 | SERVERTIME_GET: null, 34 | SERVERTIME_PUT: null, 35 | 36 | // overlay actions 37 | OVERLAY_PUSH: null, 38 | OVERLAY_POP: null, 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /public/javascripts/components/nav-bar.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Router = require('react-router'), 6 | Link = Router.Link; 7 | 8 | module.exports = React.createClass({ 9 | mixins: [Router.State], 10 | render: function () { 11 | return ( 12 | 32 | ); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /public/javascripts/components/server-time.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Stores = require('../stores'), 6 | storeChangeMixin = require('../mixins/store-change'); 7 | 8 | var ServerTimeActions = require('../actions/server-time'); 9 | 10 | module.exports = React.createClass({ 11 | 12 | mixins: [storeChangeMixin(Stores.ServerTimeStore), React.addons.PureRenderMixin], 13 | 14 | getInitialState: function () { 15 | return { 16 | time: Stores.ServerTimeStore.getServerTime() 17 | }; 18 | }, 19 | 20 | storeChange: function () { 21 | this.setState({time: Stores.ServerTimeStore.getServerTime()}); 22 | }, 23 | 24 | componentDidMount: function() { 25 | ServerTimeActions.subscribe(); 26 | }, 27 | 28 | componentWillUnmount: function() { 29 | ServerTimeActions.unsubscribe(); 30 | }, 31 | 32 | render: function () { 33 | var content; 34 | 35 | if (!this.state.time) { 36 | content =
Unknown
; 37 | } 38 | else { 39 | content =
{this.state.time.data}
; 40 | } 41 | 42 | return ( 43 |
44 |
45 |
46 |

Server Time

47 |
48 |
49 | {content} 50 |
51 |
52 |
53 | ); 54 | 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /public/javascripts/actions/server-time.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var kActions = require('../constants/actions'); 6 | var kStates = require('../constants/states'); 7 | 8 | var meteredGET = require('../common/metered-request').get; 9 | 10 | var BaseAction = require('./base'); 11 | 12 | class ItemActions extends BaseAction { 13 | 14 | constructor () { 15 | super(); 16 | 17 | // explicitly bind handlers for web socket events 18 | _.bindAll(this, '_onPut'); 19 | } 20 | 21 | // GET the time on the server 22 | getTime() { 23 | meteredGET( 24 | '/api/servertime', 25 | () => this.dispatchServerAction(kActions.SERVERTIME_GET, kStates.LOADING), 26 | data => this.dispatchServerAction(kActions.SERVERTIME_GET, kStates.SYNCED, data), 27 | err => this.dispatchServerAction(kActions.SERVERTIME_GET, kStates.ERRORED, err) 28 | ); 29 | } 30 | 31 | 32 | /** 33 | * 34 | * Server-time real-time subscription methods 35 | * 36 | */ 37 | _onPut (event, channel, data) { 38 | this.dispatchServerAction(kActions.SERVERTIME_PUT, kStates.SYNCED, data); 39 | } 40 | 41 | subscribe() { 42 | var channels = ['/api/servertime']; 43 | this._subscribe(channels, ['PUT'], this._onPut); 44 | } 45 | 46 | unsubscribe() { 47 | var channels = ['/api/servertime']; 48 | this._unsubscribe(channels, ['PUT'], this._onPut); 49 | } 50 | 51 | } 52 | 53 | module.exports = new ItemActions(); 54 | -------------------------------------------------------------------------------- /public/javascripts/components/overlays/alert.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var React = require('react/addons'); 6 | var OverlayMixin = require('../../mixins/overlay'); 7 | 8 | module.exports = React.createClass({ 9 | 10 | mixins: [OverlayMixin], 11 | 12 | render: function () { 13 | var content; 14 | 15 | if (_.isString(this.props.msg)) { 16 | // simple string message 17 | content = ( 18 |

{this.props.msg}

19 | ); 20 | } 21 | else { 22 | // message is an element 23 | content = this.props.msg; 24 | } 25 | 26 | return ( 27 |
28 |
29 |
30 |
31 | 32 |

{this.props.title || 'Alert'}

33 |
34 |
35 | {content} 36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 | ); 44 | }, 45 | 46 | 47 | handleModalHidden: function() { 48 | if (this.props.ackCallback) { 49 | this.props.ackCallback(); 50 | } 51 | }, 52 | 53 | handleOK: function () { 54 | this.hideModal(); 55 | } 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var path = require('path'); 5 | var favicon = require('serve-favicon'); 6 | var logger = require('morgan'); 7 | var cookieParser = require('cookie-parser'); 8 | var bodyParser = require('body-parser'); 9 | 10 | var api = require('./routes/api'); 11 | var routes = require('./routes/index'); 12 | 13 | var app = express(); 14 | 15 | // view engine setup 16 | app.set('views', path.join(__dirname, 'views')); 17 | app.set('view engine', 'ejs'); 18 | 19 | app.use(favicon(__dirname + '/public/images/favicon.ico')); 20 | app.use(logger('dev')); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(cookieParser()); 24 | app.use(express.static(path.join(__dirname, 'public'))); 25 | 26 | app.use('/api', api); 27 | app.use('/', routes); 28 | 29 | // catch 404 and forward to error handler 30 | app.use(function(req, res, next) { 31 | var err = new Error('Not Found'); 32 | err.status = 404; 33 | next(err); 34 | }); 35 | 36 | // error handlers 37 | 38 | // development error handler 39 | // will print stacktrace 40 | if (app.get('env') === 'development') { 41 | app.use(function(err, req, res, next) { 42 | res.status(err.status || 500); 43 | res.render('error', { 44 | message: err.message, 45 | error: err 46 | }); 47 | }); 48 | } 49 | 50 | // production error handler 51 | // no stacktraces leaked to user 52 | app.use(function(err, req, res, next) { 53 | res.status(err.status || 500); 54 | res.render('error', { 55 | message: err.message, 56 | error: {} 57 | }); 58 | }); 59 | 60 | 61 | module.exports = app; 62 | -------------------------------------------------------------------------------- /gulp/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dest = './public/javascripts/bundles'; 4 | 5 | var vendorLibs = [ 6 | 'url', 7 | 'util', 8 | 'events', 9 | 'lodash', 10 | 'jquery', 11 | 'bootstrap', 12 | 'react', 13 | 'react/addons', 14 | 'react-router', 15 | 'socket.io-client', 16 | 'es6-promise', 17 | 'moment', 18 | 'node-uuid' 19 | ]; 20 | 21 | var es6ify = require('es6ify'); 22 | var reactify = require('reactify'); 23 | 24 | module.exports = { 25 | production: process.env.NODE_ENV === 'production', 26 | bootstrap: { 27 | bootstrapHome: 'node_modules/bootstrap', 28 | fonts: './public/fonts/bootstrap' 29 | }, 30 | less: { 31 | src: './public/less/styles.less', 32 | watch: [ 33 | './public/less/**' 34 | ], 35 | dest: './public/stylesheets' 36 | }, 37 | browserify: { 38 | // Enable source maps 39 | debug: true, 40 | // A separate bundle will be generated for each 41 | // bundle config in the list below 42 | bundleConfigs: { 43 | main: { 44 | entries: './public/javascripts/main.jsx', 45 | dest: dest, 46 | outputName: 'main.js', 47 | external: vendorLibs, 48 | transforms: [reactify, es6ify.configure(/\.jsx?$/)], 49 | watch: [ 50 | 'public/javascripts/**/*.js', 51 | 'public/javascripts/**/*.jsx', 52 | '!public/javascripts/bundles/*.js', 53 | '!public/javascripts/vendor/**/*.js' 54 | ] 55 | }, 56 | vendor: { 57 | entries: './public/javascripts/noop.js', 58 | dest: dest, 59 | outputName: 'vendor.js', 60 | require: vendorLibs, 61 | add: [es6ify.runtime] 62 | } 63 | } 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flux-starter", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.12.0", 10 | "bootstrap": "^3.3.4", 11 | "bower": "^1.3.12", 12 | "cookie-parser": "~1.3.4", 13 | "debug": "~2.1.1", 14 | "ejs": "~2.3.1", 15 | "es6-promise": "^2.0.1", 16 | "express": "~4.12.2", 17 | "immutable": "^3.6.4", 18 | "jquery": "^2.1.3", 19 | "lodash": "^3.5.0", 20 | "moment": "^2.9.0", 21 | "morgan": "~1.5.1", 22 | "node-uuid": "^1.4.3", 23 | "react": "^0.12.2", 24 | "react-router": "^0.12.4", 25 | "serve-favicon": "~2.2.0", 26 | "socket.io": "^1.3.5", 27 | "socket.io-client": "^1.3.5" 28 | }, 29 | "devDependencies": { 30 | "browserify": "^9.0.3", 31 | "browserify-shim": "^3.8.3", 32 | "es6ify": "^1.6.0", 33 | "gulp": "^3.8.11", 34 | "gulp-autoprefixer": "^2.1.0", 35 | "gulp-less": "^3.0.1", 36 | "gulp-notify": "^2.2.0", 37 | "gulp-sass": "^1.3.3", 38 | "gulp-sourcemaps": "^1.5.0", 39 | "gulp-util": "^3.0.4", 40 | "pretty-hrtime": "^1.0.0", 41 | "react-tools": "^0.13.0", 42 | "reactify": "^1.0.0", 43 | "require-dir": "^0.1.0", 44 | "vinyl-source-stream": "^1.1.0", 45 | "watchify": "^2.4.0" 46 | }, 47 | "browserify": { 48 | "transform": [ 49 | "browserify-shim" 50 | ] 51 | }, 52 | "browser": { 53 | "bootstrap": "./node_modules/bootstrap/dist/js/bootstrap.js" 54 | }, 55 | "browserify-shim": { 56 | "bootstrap": { 57 | "exports": "bootstrap", 58 | "depends": [ 59 | "jquery:jQuery" 60 | ] 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/javascripts/vendor/flux/invariant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule invariant 10 | */ 11 | 12 | "use strict"; 13 | 14 | /** 15 | * Use invariant() to assert state which your program assumes to be true. 16 | * 17 | * Provide sprintf-style format (only %s is supported) and arguments 18 | * to provide information about what broke and what you were 19 | * expecting. 20 | * 21 | * The invariant message will be stripped in production, but the invariant 22 | * will remain to ensure logic does not differ in production. 23 | */ 24 | 25 | var invariant = function(condition, format, a, b, c, d, e, f) { 26 | if (false) { 27 | if (format === undefined) { 28 | throw new Error('invariant requires an error message argument'); 29 | } 30 | } 31 | 32 | if (!condition) { 33 | var error; 34 | if (format === undefined) { 35 | error = new Error( 36 | 'Minified exception occurred; use the non-minified dev environment ' + 37 | 'for the full error message and additional helpful warnings.' 38 | ); 39 | } else { 40 | var args = [a, b, c, d, e, f]; 41 | var argIndex = 0; 42 | error = new Error( 43 | 'Invariant Violation: ' + 44 | format.replace(/%s/g, function() { return args[argIndex++]; }) 45 | ); 46 | } 47 | 48 | error.framesToPop = 1; // we don't care about invariant's own frame 49 | throw error; 50 | } 51 | }; 52 | 53 | module.exports = invariant; 54 | -------------------------------------------------------------------------------- /public/javascripts/components/overlays/confirm.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var React = require('react/addons'); 6 | 7 | var OverlayMixin = require('../../mixins/overlay'); 8 | 9 | 10 | module.exports = React.createClass({ 11 | 12 | mixins: [OverlayMixin], 13 | 14 | render: function () { 15 | var content; 16 | 17 | if (_.isString(this.props.msg)) { 18 | // simple string message 19 | content = ( 20 |

{this.props.msg}

21 | ); 22 | } 23 | else { 24 | // message is an element 25 | content = this.props.msg; 26 | } 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 | 34 |

{this.props.title || 'Confirm'}

35 |
36 |
37 | {content} 38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |
46 | ); 47 | }, 48 | 49 | handleModalHidden: function() { 50 | if (this.confirmed) { 51 | if (this.props.yesCallback) { 52 | this.props.yesCallback(); 53 | } 54 | } 55 | else { 56 | if (this.props.noCallback) { 57 | this.props.noCallback(); 58 | } 59 | } 60 | }, 61 | 62 | _handleYes: function () { 63 | this.confirmed = true; 64 | this.hideModal(); 65 | }, 66 | 67 | _handleNo: function () { 68 | this.hideModal(); 69 | } 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('react-flux-starter:server'); 9 | var http = require('http'); 10 | var pubSubBridge = require('../pubsub-bridge'); 11 | 12 | /** 13 | * Get port from environment and store in Express. 14 | */ 15 | 16 | var port = normalizePort(process.env.PORT || '3000'); 17 | app.set('port', port); 18 | 19 | /** 20 | * Create HTTP server. 21 | */ 22 | 23 | var server = http.createServer(app); 24 | 25 | /** 26 | * Listen on provided port, on all network interfaces. 27 | */ 28 | 29 | server.listen(port); 30 | server.on('error', onError); 31 | server.on('listening', onListening); 32 | 33 | /** 34 | * Start pubsub-bridge which routes traffic from redis to websockets 35 | */ 36 | 37 | var io = pubSubBridge(server); 38 | 39 | /** 40 | * Normalize a port into a number, string, or false. 41 | */ 42 | 43 | function normalizePort(val) { 44 | var port = parseInt(val, 10); 45 | 46 | if (isNaN(port)) { 47 | // named pipe 48 | return val; 49 | } 50 | 51 | if (port >= 0) { 52 | // port number 53 | return port; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | /** 60 | * Event listener for HTTP server "error" event. 61 | */ 62 | 63 | function onError(error) { 64 | if (error.syscall !== 'listen') { 65 | throw error; 66 | } 67 | 68 | var bind = typeof port === 'string' 69 | ? 'Pipe ' + port 70 | : 'Port ' + port; 71 | 72 | // handle specific listen errors with friendly messages 73 | switch (error.code) { 74 | case 'EACCES': 75 | console.error(bind + ' requires elevated privileges'); 76 | process.exit(1); 77 | break; 78 | case 'EADDRINUSE': 79 | console.error(bind + ' is already in use'); 80 | process.exit(1); 81 | break; 82 | default: 83 | throw error; 84 | } 85 | } 86 | 87 | /** 88 | * Event listener for HTTP server "listening" event. 89 | */ 90 | 91 | function onListening() { 92 | var addr = server.address(); 93 | var bind = typeof addr === 'string' 94 | ? 'pipe ' + addr 95 | : 'port ' + addr.port; 96 | debug('Listening on ' + bind); 97 | } 98 | -------------------------------------------------------------------------------- /gulp/util/bundler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserify = require('browserify'); 4 | var watchify = require('watchify'); 5 | var bundleLogger = require('./bundleLogger'); 6 | var gulp = require('gulp'); 7 | var handleErrors = require('./handleErrors'); 8 | var source = require('vinyl-source-stream'); 9 | var config = require('../config').browserify; 10 | 11 | module.exports = function(bundleConfig) { 12 | 13 | var bundler = browserify({ 14 | // Required watchify args 15 | cache: {}, packageCache: {}, fullPaths: false, 16 | // Specify the entry point of your app 17 | entries: bundleConfig.entries, 18 | // Add file extentions to make optional in your requires 19 | extensions: config.extensions, 20 | // Enable source maps! 21 | debug: config.debug 22 | }); 23 | 24 | if (bundleConfig.add) { 25 | bundleConfig.add.forEach(function(lib) { bundler.add(lib); }); 26 | } 27 | 28 | if (bundleConfig.require) { 29 | bundleConfig.require.forEach(function(lib) { bundler.require(lib); }); 30 | } 31 | 32 | if (bundleConfig.external) { 33 | bundleConfig.external.forEach(function(lib) { bundler.external(lib); }); 34 | } 35 | 36 | if (bundleConfig.transforms) { 37 | bundleConfig.transforms.forEach(function(transform) { bundler.transform(transform); }); 38 | } 39 | 40 | var bundle = function() { 41 | // Log when bundling starts 42 | bundleLogger.start(bundleConfig.outputName); 43 | 44 | return bundler 45 | .bundle() 46 | // Report compile errors 47 | .on('error', handleErrors) 48 | // Use vinyl-source-stream to make the 49 | // stream gulp compatible. Specifiy the 50 | // desired output filename here. 51 | .pipe(source(bundleConfig.outputName)) 52 | // Specify the output destination 53 | .pipe(gulp.dest(bundleConfig.dest)) 54 | .on('end', reportFinished); 55 | }; 56 | 57 | if(global.isWatching) { 58 | // Wrap with watchify and rebundle on changes 59 | bundler = watchify(bundler); 60 | // Rebundle on update 61 | bundler.on('update', bundle); 62 | } 63 | 64 | var reportFinished = function() { 65 | // Log when bundling completes 66 | bundleLogger.end(bundleConfig.outputName); 67 | }; 68 | 69 | return bundle(); 70 | }; 71 | -------------------------------------------------------------------------------- /public/javascripts/actions/overlays.js: -------------------------------------------------------------------------------- 1 | /** @module actions/status */ 2 | 3 | 'use strict'; 4 | 5 | var React = require('react/addons'); 6 | 7 | var Actions = require('./base'); 8 | 9 | var kActions = require('../constants/actions'), 10 | kStates = require('../constants/states'); 11 | 12 | /** 13 | * Overlays are basic modals or popups. Various alerts and confirms are predefined. 14 | */ 15 | 16 | class OverlaysActions extends Actions { 17 | 18 | /** 19 | * push new overlay component onto Overlay stack 20 | * @method push 21 | */ 22 | push(component) { 23 | //console.debug('OverlaysActions.push'); 24 | var that = this; 25 | return new Promise(function (resolve) { 26 | process.nextTick(function () { 27 | that.dispatchViewAction(kActions.OVERLAY_PUSH, kStates.SYNCED, {component: component}); 28 | resolve(); 29 | }); 30 | }); 31 | } 32 | 33 | /** 34 | * pop top overlay from Overlay stack 35 | * @method pop 36 | */ 37 | pop() { 38 | //console.debug('OverlaysActions.pop'); 39 | this.dispatchViewAction(kActions.OVERLAY_POP, kStates.SYNCED, null); 40 | } 41 | 42 | /** 43 | * display a simple 'alert' modal 44 | * @method alert 45 | */ 46 | alert(title, msg, ackCallback) { 47 | var alertOverlay = React.createFactory(require('../components/overlays/alert.jsx')); 48 | return this.push(alertOverlay({ 49 | title: title, 50 | msg: msg, 51 | ackCallback: ackCallback 52 | }, null)); 53 | } 54 | 55 | /** 56 | * display a simple modal with an informational message 57 | * @method info 58 | */ 59 | info(msg, title) { 60 | return this.alert(title || 'Info', msg); 61 | } 62 | 63 | /** 64 | * display a simple modal with an error message 65 | * @method error 66 | */ 67 | error(msg, title) { 68 | return this.alert(title || 'Error', msg); 69 | } 70 | 71 | /** 72 | * display a simple modal with a warning message 73 | * @method warn 74 | */ 75 | warn(msg, title) { 76 | return this.alert(title || 'Warning', msg); 77 | } 78 | 79 | 80 | /** 81 | * display a simple 'confirm' modal with yes/no responses 82 | * @method confirm 83 | */ 84 | confirm(title, msg, yesCallback, noCallback) { 85 | var confirmOverlay = React.createFactory(require('../components/overlays/confirm.jsx')); 86 | return this.push(confirmOverlay({ 87 | title: title, 88 | msg: msg, 89 | yesCallback: yesCallback, 90 | noCallback: noCallback 91 | }, null)); 92 | } 93 | 94 | } 95 | 96 | module.exports = new OverlaysActions(); 97 | -------------------------------------------------------------------------------- /public/javascripts/components/overlays/item-form.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | var OverlayMixin = require('../../mixins/overlay'); 6 | 7 | 8 | module.exports = React.createClass({ 9 | 10 | mixins: [OverlayMixin, React.addons.LinkedStateMixin], 11 | 12 | getInitialState: function () { 13 | return { 14 | firstName: this.props.firstName || '', 15 | lastName: this.props.lastName || '' 16 | }; 17 | }, 18 | 19 | render: function () { 20 | return ( 21 |
22 |
23 |
24 |
25 | 26 |

Add New Item

27 |
28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 |
47 | ); 48 | }, 49 | 50 | // uses Bootstrap modal's 51 | handleModalShown: function() { 52 | this.refs.firstName.getDOMNode().focus(); 53 | }, 54 | 55 | handleModalHidden: function() { 56 | if (this.confirmed) { 57 | if (this.props.okCallback) { 58 | this.props.okCallback(this.state.firstName, this.state.lastName); 59 | } 60 | } 61 | else { 62 | if (this.props.cancelCallback) { 63 | this.props.cancelCallback(); 64 | } 65 | } 66 | }, 67 | 68 | _handleAdd: function () { 69 | this.confirmed = true; 70 | this.hideModal(); 71 | }, 72 | 73 | _handleCancel: function () { 74 | this.hideModal(); 75 | } 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /public/javascripts/stores/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | 5 | var _ = require('lodash'); 6 | var Immutable = require('immutable'); 7 | 8 | const CHANGE_EVENT = 'change'; 9 | 10 | class BaseStore extends EventEmitter { 11 | constructor(dispatcher) { 12 | super(); 13 | this.dispatcher = dispatcher; 14 | this.inFlight = false; 15 | this.error = null; 16 | this._register(); 17 | this.initialize(); 18 | } 19 | 20 | initialize() {} 21 | 22 | addChangeListener(callback) { 23 | this.on(CHANGE_EVENT, callback); 24 | } 25 | 26 | removeChangeListener(callback) { 27 | this.removeListener(CHANGE_EVENT, callback); 28 | } 29 | 30 | emitChange() { 31 | this.emit(CHANGE_EVENT); 32 | } 33 | 34 | getState() { 35 | return undefined; 36 | } 37 | 38 | isInFlight() { 39 | return this.inFlight; 40 | } 41 | 42 | _getActions(){ 43 | return {}; 44 | } 45 | 46 | getStoreName() { 47 | return this.constructor.name; 48 | } 49 | 50 | // for creating a standard store entry that captures the entities state 51 | makeStatefulEntry(state=undefined, data=undefined) { 52 | return { 53 | state: state, 54 | data: data 55 | }; 56 | } 57 | 58 | updateStatefulEntry(entry, state, data) { 59 | _.extend(entry.data || (entry.data = {}), data); 60 | entry.state = state; 61 | return entry; 62 | } 63 | 64 | _register() { 65 | this.dispatchToken = this.dispatcher.register(_.bind(function (payload) { 66 | this._handleAction(payload.action.actionType, payload.action); 67 | }, this)); 68 | } 69 | 70 | _handleAction(actionType, action){ 71 | // Proxy actionType to the instance method defined in actions[actionType], 72 | // or optionally if the value is a function, invoke it instead. 73 | var actions = this._getActions(); 74 | if (actions.hasOwnProperty(actionType)) { 75 | var actionValue = actions[actionType]; 76 | if (_.isString(actionValue)) { 77 | if (_.isFunction(this[actionValue])) { 78 | this[actionValue](action); 79 | } 80 | else { 81 | throw new Error(`Action handler defined in Store map is undefined or not a Function. Store: ${this.constructor.name}, Action: ${actionType}`); 82 | } 83 | } 84 | else if (_.isFunction(actionValue)) { 85 | actionValue.call(this, action); 86 | } 87 | } 88 | } 89 | 90 | _makeStoreEntry() { 91 | return Immutable.fromJS({ 92 | _meta: { 93 | state: undefined 94 | } 95 | }); 96 | } 97 | 98 | } 99 | 100 | module.exports = BaseStore; 101 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var express = require('express'); 6 | 7 | var router = express.Router(); 8 | 9 | 10 | var _items = { 11 | 'dd586160-cd1d-11e4-aaba-2566638c135c': {id: 'dd586160-cd1d-11e4-aaba-2566638c135c', first: 'John', last: 'Doe'}, 12 | '42780a30-ce56-11e4-b45f-2d14dab59bc8': {id: '42780a30-ce56-11e4-b45f-2d14dab59bc8', first: 'Frank', last: 'Wells'}, 13 | '80713690-ce56-11e4-b45f-2d14dab59bc8': {id: '80713690-ce56-11e4-b45f-2d14dab59bc8', first: 'Cindy', last: 'LooHoo'} 14 | }; 15 | 16 | // response headers for all responses 17 | router.use(function (req, res, next) { 18 | res.set('Cache-Control', 'public, max-age=0'); 19 | next(); 20 | }); 21 | 22 | 23 | /** 24 | * items endpoint handlers 25 | */ 26 | 27 | router.route('/items') 28 | 29 | // GET list of items 30 | .get(function (req, res) { 31 | res.status(200).send(_.map(_items, function(item) { return item; })); 32 | }) 33 | 34 | // POST new item 35 | .post(function (req, res) { 36 | var item = _.extend({}, req.body); 37 | _items[item.id] = item; 38 | 39 | // simulate slow response 40 | setTimeout(function () { res.status(200).send(item); }, 1000); 41 | 42 | // io.emit(req.user.id, 'POST /api/items/' + item.id, item); 43 | }); 44 | 45 | 46 | 47 | // validate id; lookup item in table 48 | router.param('id', function (req, res, next, id) { 49 | var item = _items[id]; 50 | if (item) { 51 | req.params.item = item; 52 | next(); 53 | } 54 | else { 55 | return res.send(404, 'NOT FOUND'); 56 | } 57 | }); 58 | 59 | router.route('/items/:id') 60 | 61 | // GET item details 62 | .get(function(req, res) { 63 | res.send(req.params.item); 64 | }) 65 | 66 | // PUT (update) details for the specified item 67 | .put(function (req, res) { 68 | 69 | var item = req.params.item; 70 | 71 | _.extend(item, req.body); 72 | 73 | _items[item.id] = item; // this is necessary but just here for example 74 | 75 | // simulate slow response 76 | setTimeout(function () { res.status(200).send(item); }, 1000); 77 | 78 | // io.emit(req.user.id, 'PUT /api/items/' + id, item); 79 | }) 80 | 81 | // DELETE the specified item 82 | .delete(function (req, res) { 83 | 84 | var id = req.params.item.id, 85 | resp = {id: id}; 86 | 87 | delete _items[id]; 88 | 89 | setTimeout(function () { res.status(200).send(resp); }, 1000); 90 | 91 | // io.emit(req.user.id, 'DELETE /api/items/' + id, {id: id}); 92 | }); 93 | 94 | 95 | /** 96 | * Server-time endpoint handlers 97 | */ 98 | router.get('/api/servertime', function (req, res) { 99 | res.json(new Date().toString()); 100 | }); 101 | 102 | module.exports = router; 103 | -------------------------------------------------------------------------------- /public/javascripts/services/api-subscriptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require("events").EventEmitter; 4 | 5 | var _ = require('lodash'); 6 | var io = require('socket.io-client'); 7 | 8 | var httpVerbRe = /^(GET|PUT|POST|DELETE)\s/; 9 | 10 | 11 | class SubscriptionService extends EventEmitter { 12 | constructor(accessToken) { 13 | super(); 14 | 15 | this.accessToken = accessToken; 16 | 17 | var socket = this.socket = io({transports:['websocket']}); 18 | 19 | // attach handlers 20 | socket.on('connect', this.handleConnect.bind(this)); 21 | socket.on('disconnect', this.handleDisconnect.bind(this)); 22 | socket.on('reconnect', this.handleReconnect.bind(this)); 23 | socket.on('set', this.handleSet.bind(this)); 24 | 25 | //console.log(socket); 26 | } 27 | 28 | handleConnect(){ 29 | this.socket.emit('auth', this.accessToken); 30 | this.emit('connect'); 31 | } 32 | 33 | handleDisconnect(){ 34 | this.emit('disconnect'); 35 | } 36 | 37 | handleReconnect(){ 38 | //console.debug('reconnect; attempts: ' + attempts); 39 | _.each(this._events, function(fn, channel){ 40 | // on reconnect remove all API channel listeners 41 | if (httpVerbRe.test(channel)) { 42 | this.removeAllListeners(channel); 43 | } 44 | }, this); 45 | this.emit('reconnect'); 46 | } 47 | 48 | handleSet(data){ 49 | this.emit(data.channel, 'set', data.channel, JSON.parse(data.data)); 50 | } 51 | 52 | subscribe(channel, handler, options){ 53 | //console.log('subscribe', arguments); 54 | 55 | // only one subscription per channel 56 | if (EventEmitter.listenerCount(this, channel) !== 0) { 57 | throw new Error('api-subscription: Cannot subscribe to channel "' + channel + '" more than once.'); 58 | } 59 | 60 | options = _.extend({ 61 | initialPayload: false, 62 | // deprecated 63 | reconnectPayload: false 64 | }, options || {}); 65 | 66 | handler._options = options; 67 | 68 | this.addListener(channel, handler); 69 | this.socket.emit('subscribe', channel, options.initialPayload); 70 | 71 | return this; 72 | } 73 | 74 | unsubscribe(channel, handler){ 75 | //console.log('unsubscribe', arguments); 76 | 77 | this.removeListener(channel, handler); 78 | 79 | // if there's no more handlers for this channel, unsubscribe from it completely 80 | if (EventEmitter.listenerCount(this, channel) === 0) { 81 | this.socket.emit('unsubscribe', channel); 82 | } 83 | return this; 84 | } 85 | 86 | normalizeChannelName(channel){ 87 | return channel.replace(/^(GET|PUT|POST|DELETE)\s/, ''); 88 | } 89 | 90 | isConnected(){ 91 | return this.socket.connected; 92 | } 93 | 94 | isDisconnected(){ 95 | return this.socket.disconnected; 96 | } 97 | } 98 | 99 | 100 | module.exports = function (accessToken) { 101 | return new SubscriptionService(accessToken); 102 | }; 103 | -------------------------------------------------------------------------------- /public/javascripts/common/metered-request.js: -------------------------------------------------------------------------------- 1 | /** @module common/metered-request */ 2 | 3 | 'use strict'; 4 | 5 | 6 | /* 7 | * 8 | * Allows only 1 request for a method/url to occur at a time. Requests for the same resource 9 | * are folded into the outstanding request by returning its Promise. 10 | * 11 | */ 12 | 13 | var _ = require('lodash'); 14 | var ajax = require('./ajax'); 15 | 16 | // Dictionary that holds in-flight requests. Maps request url to promise. 17 | var _inFlightRequests = {}; 18 | 19 | module.exports = { 20 | 21 | /** GET JSON resource from API endpoint. 22 | * If request is already pending, will return the existing promise. 23 | * @method get 24 | * @param {string or object} url | settings - either: 25 | * a string containing the URL to which the request is sent or 26 | * a set of key/value pairs that configure the Ajax request 27 | * @returns {Promise} 28 | */ 29 | get: function (settings, startHdlr, resolveHdlr, rejectHdlr, apiOpts) { 30 | var url; 31 | var promise; 32 | 33 | if (_.isString(settings)) { 34 | url = settings; 35 | settings = { 36 | url: url, 37 | contentType : 'application/json', 38 | type : 'GET' 39 | }; 40 | } 41 | else { 42 | url = settings.url; 43 | settings = _.extend({}, settings, { 44 | contentType : 'application/json', 45 | type : 'GET' 46 | }); 47 | } 48 | 49 | if (!_.isString(url)) { 50 | throw new Error('metered-request::get - URL argument is not a string'); 51 | } 52 | 53 | // request already in flight, return its promise 54 | if (url in _inFlightRequests) { 55 | //console.debug('Returning pending metered request for: ' + url); 56 | promise = _inFlightRequests[url]; 57 | promise._isNew = false; 58 | return promise; 59 | } 60 | 61 | //console.debug('Creating new metered request for: ' + url); 62 | 63 | // create a new promise to represent the GET. GETs are always 64 | // initiated in the nextTick to prevent dispatches during dispatches 65 | // 66 | promise = new Promise(function (resolve, reject) { 67 | 68 | process.nextTick(function () { 69 | 70 | ajax(settings, apiOpts) 71 | .then(function (data) { 72 | delete _inFlightRequests[url]; 73 | resolveHdlr(data); 74 | resolve(data); 75 | }, function (err) { 76 | delete _inFlightRequests[url]; 77 | rejectHdlr(err); 78 | reject(err); 79 | }); 80 | 81 | startHdlr(); 82 | }); 83 | }); 84 | 85 | promise.catch(function () { 86 | // no-op catch handler to prevent "unhandled promise rejection" console messages 87 | }); 88 | 89 | // Add a custom property to assert that a AJAX request 90 | // was make and a Promise created as part of this call. 91 | // This is used as a hint to callers to create resolve/reject 92 | // handlers or not (i.e. don't add more resolve/reject handlers 93 | // if the Promise is for a pending request) 94 | promise._isNew = true; 95 | 96 | // record request 97 | _inFlightRequests[url] = promise; 98 | 99 | return promise; 100 | } 101 | 102 | }; 103 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Settings 3 | "maxerr" : 100, // Maximum error before stopping. 4 | 5 | 6 | // Predefined globals whom JSHint will ignore. 7 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 8 | 9 | "node" : true, 10 | "rhino" : false, 11 | "couch" : false, 12 | "wsh" : true, // Windows Scripting Host. 13 | 14 | //"jquery" : true, 15 | "prototypejs" : false, 16 | "mootools" : false, 17 | "dojo" : false, 18 | 19 | "predef" : [ 20 | "CIT" 21 | ], 22 | 23 | // Development. 24 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 25 | "devel" : true, // Allow developments statements e.g. `console.log();`. 26 | 27 | 28 | // ECMAScript 5. 29 | //"es5" : true, // Allow ECMAScript 5 syntax. 30 | "strict" : true, // Require `use strict` pragma in every file. 31 | "globalstrict" : true, // Allow global "use strict" (also enables 'strict'). 32 | 33 | // ECMAScript 6. 34 | "esnext" : true, // Allow ECMAScript 6 syntax. 35 | 36 | // The Good Parts. 37 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 38 | "laxbreak" : true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 39 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 40 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 41 | "curly" : true, // Require {} for every new block or scope. 42 | "eqeqeq" : true, // Require triple equals i.e. `===`. 43 | "eqnull" : false, // Tolerate use of `== null`. 44 | "evil" : false, // Tolerate use of `eval`. 45 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 46 | "forin" : false, // Tolerate `for in` loops without `hasOwnPrototype`. 47 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 48 | "latedef" : true, // Prohipit variable use before definition. 49 | "loopfunc" : false, // Allow functions to be defined within loops. 50 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 51 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 52 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 53 | "scripturl" : true, // Tolerate script-targeted URLs. 54 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 55 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 56 | "undef" : true, // Require all non-global variables be declared before they are used. 57 | "unused" : true, // Warns when you define and never use your variables 58 | 59 | 60 | // Personal styling preferences. 61 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 62 | "noempty" : true, // Prohibit use of empty blocks. 63 | "nonew" : true, // Prohibit use of constructors for side-effects. 64 | "plusplus" : false, // Prohibit use of `++` & `--`. 65 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 66 | "trailing" : true, // Prohibit trailing whitespaces. 67 | "indent" : 2 // Specify indentation spacing 68 | } 69 | -------------------------------------------------------------------------------- /public/javascripts/components/items.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var React = require('react/addons'); 6 | 7 | var Stores = require('../stores'), 8 | storeChangeMixin = require('../mixins/store-change'); 9 | 10 | var kStates = require('../constants/states'); 11 | 12 | var ItemActions = require('../actions/items'); 13 | 14 | var Overlays = require('../actions/overlays'); 15 | 16 | /** 17 | * Simple example of a basic CRUD interface. List items and supports 18 | * create, edit, delete of items in list. 19 | * 20 | * Uses the CRUD based Item Store and Actions. Listens to changes to the 21 | * Item store. Uses Reacts PureRenderMixin to only render when state or 22 | * props have changed (would work even better if using Immutable JS) 23 | */ 24 | 25 | module.exports = React.createClass({ 26 | 27 | mixins: [storeChangeMixin(Stores.ItemsStore), React.addons.PureRenderMixin], 28 | 29 | getInitialState: function () { 30 | return { 31 | items: Stores.ItemsStore.getAll(), 32 | selection: null 33 | }; 34 | }, 35 | 36 | storeChange: function () { 37 | this.setState({items: Stores.ItemsStore.getAll()}); 38 | }, 39 | 40 | render: function () { 41 | 42 | var content; 43 | 44 | if (!this.state.items) { 45 | content =
Loading...
; 46 | } 47 | else { 48 | content = ( 49 | 50 | 51 | 52 | 53 | 54 | {_.map(this.state.items, item => 55 | 61 | 62 | 63 | )} 64 | 65 |
First NameLast NameId
{item.data.first}{item.data.last}{item.data.id}
66 | ); 67 | } 68 | 69 | return ( 70 |
71 |
72 |
73 |
74 |

Items

75 | 78 | 81 | 84 |
85 |
86 |
87 | {content} 88 |
89 |
90 |
91 | ); 92 | }, 93 | 94 | _onClick: function (id) { 95 | this.setState({selection: id}); 96 | }, 97 | 98 | _onAdd: function () { 99 | var overlay = React.createFactory(require('./overlays/item-form.jsx')); 100 | Overlays.push(overlay({ 101 | okCallback: (firstName, lastName) => { 102 | var id = ItemActions.post(firstName, lastName); 103 | this.setState({selection: id}); 104 | console.log(`create new item #${id}`); 105 | } 106 | }, null)); 107 | }, 108 | 109 | _onUpdate: function () { 110 | var item = Stores.ItemsStore.get(this.state.selection); 111 | var overlay = React.createFactory(require('./overlays/item-form.jsx')); 112 | return Overlays.push(overlay({ 113 | firstName: item.data.first, 114 | lastName: item.data.last, 115 | okCallback: (firstName, lastName) => ItemActions.put(this.state.selection, firstName, lastName) 116 | }, null)); 117 | }, 118 | 119 | _onDelete: function () { 120 | ItemActions.delete(this.state.selection); 121 | this.setState({selection: null}); 122 | } 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /public/javascripts/actions/base.js: -------------------------------------------------------------------------------- 1 | /** @module actions/base */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var Dispatcher = require('../dispatcher'); 7 | 8 | var apiSubscriptionSrvc = require('../services/index').apiSubscriptions; 9 | 10 | 11 | module.exports = class Actions { 12 | 13 | constructor () { 14 | this._dispatcher = Dispatcher; 15 | } 16 | 17 | /** 18 | * Standard Dispatcher payload 19 | * 20 | * @property {integer} actionType - action type from constants/actions 21 | * @property {integer} syncState - sync state with server from contants/states 22 | * @property {object} data - payload data (to be interpreted based on actionType & state) 23 | * 24 | */ 25 | _makePayload (action, syncState, data) { 26 | return { 27 | actionType: action, 28 | syncState: syncState, 29 | data: data 30 | }; 31 | } 32 | 33 | /** 34 | * Generate list of physical channels 35 | * @method _getPhysicalChannels 36 | * @param {array} channels 37 | * @param {array} methods 38 | */ 39 | _getPhysicalChannels (channels, methods) { 40 | return _.flatten(channels.map(function (channel) { 41 | return methods.map(function (method) { 42 | return method + ' ' + channel; 43 | }); 44 | })); 45 | } 46 | 47 | /** Subscribe to channel. 48 | * @method _subscribe 49 | * @param {string|array} channels - String or array of channel names. 50 | * @param {array} methods 51 | * @param {function} handler - Handler to be called when event on channel occurs 52 | */ 53 | _subscribe (channels, methods, handler, options) { 54 | 55 | if (_.isString(channels)) { 56 | channels = [channels]; 57 | } 58 | 59 | if (!_.isArray(methods)) { 60 | throw new Error('methods argument must be array of HTTP methods'); 61 | } 62 | 63 | _.each(this._getPhysicalChannels(channels, methods), function (channel) { 64 | apiSubscriptionSrvc.subscribe(channel, handler, options); 65 | }); 66 | 67 | } 68 | 69 | /** Unsubscribe from channel. 70 | * @method _unsubscribe 71 | * @param {string|array} channels - String or array of channel names. 72 | * @param {array} methods 73 | * @param {function} handler - Handler to be called when event on channel occurs 74 | */ 75 | _unsubscribe (channels, methods, handler) { 76 | 77 | if (_.isString(channels)) { 78 | channels = [channels]; 79 | } 80 | 81 | if (!_.isArray(methods)) { 82 | throw new Error('methods argument must be array of HTTP methods'); 83 | } 84 | 85 | _.each(this._getPhysicalChannels(channels, methods), function (channel) { 86 | apiSubscriptionSrvc.unsubscribe(channel, handler); 87 | }); 88 | 89 | } 90 | 91 | /** Clean leading HTTP verbs from a channel name. 92 | * @method _normalizeChannelName 93 | * @param {string} channel 94 | */ 95 | _normalizeChannelName(channel){ 96 | return apiSubscriptionSrvc.normalizeChannelName(channel); 97 | } 98 | 99 | _checkDispatchArgs (action, syncState) { 100 | if (action === undefined) { 101 | throw new Error(`action argument value of undefined passed to dispatchUserAction. You're most likely referencing an invalid Action constant (constants/actions.js).`); 102 | } 103 | if (syncState === undefined) { 104 | throw new Error(`syncState argument value of undefined passed to dispatchUserAction. You're most likely referencing an invalid State constant (constants/state.js).`); 105 | } 106 | } 107 | 108 | /** Dispatch server action. 109 | * @method dispatchServerAction 110 | * @param {integer} actionType - action type from constants/actions 111 | * @param {integer} syncState - sync state with server; one of SYNCED, REQUEST, ERRORED from contants/states 112 | * @param {object} data - payload data (to be interpreted based on actionType & state) 113 | */ 114 | dispatchServerAction (action, syncState, data) { 115 | this._checkDispatchArgs(action, syncState); 116 | try { 117 | this._dispatcher.handleServerAction(this._makePayload(action, syncState, data)); 118 | } 119 | catch (err) { 120 | console.error(err.stack); 121 | } 122 | } 123 | 124 | /** Dispatch user action. 125 | * @method dispatchUserAction 126 | * @param {integer} actionType - action type from constants/actions 127 | * @param {integer} syncState - sync state with server; one of SYNCED, REQUEST, ERRORED from contants/states 128 | * @param {object} data - payload data (to be interpreted based on actionType & state) 129 | */ 130 | dispatchViewAction (action, syncState, data) { 131 | this._checkDispatchArgs(action, syncState); 132 | try { 133 | this._dispatcher.handleViewAction(this._makePayload(action, syncState, data)); 134 | } 135 | catch (err) { 136 | console.log(err.stack); 137 | } 138 | } 139 | 140 | }; 141 | -------------------------------------------------------------------------------- /public/javascripts/stores/crud-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var BaseStore = require('./base'); 6 | 7 | var kActions = require('../constants/actions'), 8 | kStates = require('../constants/states'); 9 | 10 | 11 | function _actionForMethod(actionObjectId, method) { 12 | return kActions[actionObjectId + '_' + method]; 13 | } 14 | 15 | class CRUDStore extends BaseStore { 16 | 17 | constructor(dispatcher, actions, actionObjectId) { 18 | super(dispatcher); 19 | 20 | this._resources = undefined; 21 | 22 | this._actions = actions; 23 | this._handlers = this._getActionHandlers(actionObjectId); 24 | 25 | // subscribe the store to the 'getAll' list endpoint 26 | actions.subscribeList(); 27 | } 28 | 29 | /** 30 | * Get array of all resources 31 | */ 32 | getAll() { 33 | return this._resources !== undefined ? _.map(this._resources, resource => resource) : this._loadAll(); 34 | } 35 | 36 | /** 37 | * Get single resource 38 | */ 39 | get(id) { 40 | return this._resources !== undefined ? (id in this._resources ? this._resources[id] : this._loadOne(id)) : this._loadAll(); 41 | } 42 | 43 | 44 | /** 45 | * Support for defining and getting action handler map 46 | */ 47 | _getActions() { 48 | return this._handlers; 49 | } 50 | 51 | _getActionHandlers(actionObjectId) { 52 | return _.zipObject([ 53 | [_actionForMethod(actionObjectId, 'GETALL'), '_onGetAll'], 54 | [_actionForMethod(actionObjectId, 'GETONE'), '_onGetOne'], 55 | [_actionForMethod(actionObjectId, 'POST'), '_onPost'], 56 | [_actionForMethod(actionObjectId, 'PUT'), '_onPut'], 57 | [_actionForMethod(actionObjectId, 'DELETE'), '_onDelete'] 58 | ]); 59 | } 60 | 61 | 62 | /** 63 | * Utility methods 64 | */ 65 | _loadAll() { 66 | this._actions.getAll(); 67 | return undefined; 68 | } 69 | 70 | _loadOne(id) { 71 | this._actions.get(id); 72 | return undefined; 73 | } 74 | 75 | /** 76 | * 77 | * Action Handlers 78 | * 79 | */ 80 | _onGetAll(payload) { 81 | console.debug(`${this.getStoreName()}:_onGetAll; state=${payload.syncState}`); 82 | 83 | switch(payload.syncState) { 84 | case kStates.LOADING: 85 | this.inflight = true; 86 | break; 87 | case kStates.SYNCED: 88 | 89 | // unsubscribe from resource websocket events if we already have resources 90 | if (this._resources) { 91 | this._actions.unsubscribeResources(_.map(this._resources, resource => resource.data.id)); 92 | } 93 | 94 | // subscribe to resource websocket events 95 | var map = _.map(payload.data, item => [item.id, this.makeStatefulEntry(payload.syncState, item)]); 96 | this._resources = _.zipObject(map); 97 | 98 | this._actions.subscribeResources(_.map(this._resources, resource => resource.data.id)); 99 | 100 | this.inflight = false; 101 | break; 102 | } 103 | 104 | this.emitChange(); 105 | } 106 | 107 | _onGetOne(payload) { 108 | console.debug(`${this.getStoreName()}:_onGetAll; state=${payload.syncState}`); 109 | 110 | var exists; 111 | 112 | switch(payload.syncState) { 113 | case kStates.LOADING: 114 | this.inflight = true; 115 | break; 116 | case kStates.SYNCED: 117 | this._resources = this._resources || {}; 118 | exists = !!this._resources[payload.data.id]; 119 | this._resources[payload.data.id] = this.makeStatefulEntry(payload.syncState, payload.data); 120 | // only subscribe to resource websocket events if the resource is new 121 | if (!exists) { 122 | this._actions.subscribeResources(payload.data.id); 123 | } 124 | this.inflight = false; 125 | break; 126 | } 127 | 128 | this.emitChange(); 129 | 130 | } 131 | 132 | _onPost(payload) { 133 | console.debug(`${this.getStoreName()}:_onPost; state=${payload.syncState}`); 134 | 135 | if (!this._resources) { this._resources = {}; } 136 | 137 | this._resources[payload.data.id] = this.makeStatefulEntry(payload.syncState, payload.data); 138 | 139 | // subscribe to resource only on SYNC 140 | if (payload.syncState === kStates.SYNCED) { 141 | this._actions.subscribeResources(payload.data.id); 142 | } 143 | 144 | this.emitChange(); 145 | } 146 | 147 | _onPut(payload) { 148 | console.debug(`${this.getStoreName()}:_onPut; state=${payload.syncState}`); 149 | 150 | if (!this._resources) { this._resources = {}; } 151 | 152 | var existingEntry = this._resources[payload.data.id]; 153 | 154 | // can only update an entry we know about ??? 155 | if (!existingEntry) { 156 | return; 157 | } 158 | 159 | this._resources[payload.data.id] = this.updateStatefulEntry(existingEntry, payload.syncState, payload.data); 160 | 161 | this.emitChange(); 162 | } 163 | 164 | _onDelete(payload) { 165 | console.debug(`${this.getStoreName()}:_onDelete; state=${payload.syncState}`); 166 | 167 | if (!this._resources) { this._resources = {}; } 168 | 169 | var existingEntry = this._resources[payload.data.id]; 170 | 171 | // can only delete an entry we know about 172 | if (!existingEntry) { 173 | return; 174 | } 175 | 176 | if (existingEntry) { 177 | switch(payload.syncState) { 178 | case kStates.DELETING: 179 | existingEntry = this.updateStatefulEntry(existingEntry, payload.syncState); 180 | break; 181 | case kStates.SYNCED: 182 | // unsubscribe from resource 183 | this._actions.unsubscribeResources(payload.data.id); 184 | delete this._resources[payload.data.id]; 185 | break; 186 | } 187 | 188 | this.emitChange(); 189 | } 190 | } 191 | 192 | } 193 | 194 | module.exports = CRUDStore; 195 | -------------------------------------------------------------------------------- /public/javascripts/actions/crud-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var uuid = require('node-uuid'); 6 | 7 | var kActions = require('../constants/actions'); 8 | var kStates = require('../constants/states'); 9 | 10 | var ajax = require('../common/ajax'); 11 | var meteredGET = require('../common/metered-request').get; 12 | 13 | var BaseAction = require('./base'); 14 | 15 | class CRUDBase extends BaseAction { 16 | 17 | constructor (baseURL, actionObjectId) { 18 | super(); 19 | this.baseURL = baseURL; 20 | this.actionObjectId = actionObjectId; 21 | 22 | // explicitly bind handlers for web socket events 23 | _.bindAll(this, '_onPostEvent', '_onPutEvent', '_onDeleteEvent'); 24 | } 25 | 26 | _actionForMethod(method) { 27 | return kActions[this.actionObjectId + '_' + method]; 28 | } 29 | 30 | /** 31 | * GET list of resources 32 | * @method 33 | */ 34 | getAll() { 35 | var action = this._actionForMethod('GETALL'); 36 | meteredGET( 37 | this.baseURL, 38 | () => this.dispatchServerAction(action, kStates.LOADING), 39 | data => this.dispatchServerAction(action, kStates.SYNCED, data), 40 | err => this.dispatchServerAction(action, kStates.ERRORED, err) 41 | ); 42 | } 43 | 44 | /** 45 | * POST (create) new resource 46 | * @method 47 | * @returns client generated UUID of the new resource 48 | */ 49 | post(payload) { 50 | 51 | var action = this._actionForMethod('POST'); 52 | 53 | payload.id = uuid.v1(); 54 | 55 | ajax({ 56 | url: this.baseURL, 57 | type: "POST", 58 | data: payload, 59 | accepts: { 60 | 'json': "application/json", 61 | 'text': 'text/plain' 62 | } 63 | }) 64 | .then(function (data) { 65 | this.dispatchServerAction(action, kStates.SYNCED, data); 66 | }.bind(this)) 67 | .catch(function (err) { 68 | this.dispatchServerAction(action, kStates.ERRORED, err); 69 | }.bind(this)); 70 | 71 | this.dispatchServerAction(action, kStates.NEW, payload); 72 | 73 | return payload.id; 74 | } 75 | 76 | /** 77 | * GET a single resource 78 | * @method 79 | */ 80 | get(id) { 81 | var action = this._actionForMethod('GETONE'); 82 | meteredGET( 83 | `${this.baseURL}/${id}`, 84 | () => this.dispatchServerAction(action, kStates.LOADING, {id: id}), 85 | data => this.dispatchServerAction(action, kStates.SYNCED, data), 86 | err => this.dispatchServerAction(action, kStates.ERRORED, err) 87 | ); 88 | } 89 | 90 | /** 91 | * PUT (update) a resource 92 | * @method 93 | */ 94 | put(id, payload) { 95 | var action = this._actionForMethod('PUT'); 96 | ajax({ 97 | url: `${this.baseURL}/${id}`, 98 | type: "PUT", 99 | data: payload, 100 | accepts: { 101 | 'json': "application/json", 102 | 'text': 'text/plain' 103 | } 104 | }) 105 | .then(function (data) { 106 | this.dispatchServerAction(action, kStates.SYNCED, data); 107 | }.bind(this)) 108 | .catch(function (err) { 109 | this.dispatchServerAction(action, kStates.ERRORED, err); 110 | }.bind(this)); 111 | 112 | this.dispatchServerAction(action, kStates.SAVING, payload); 113 | } 114 | 115 | /** 116 | * DELETE a resource 117 | * @method 118 | */ 119 | delete(id) { 120 | var action = this._actionForMethod('DELETE'); 121 | ajax({ 122 | url: `${this.baseURL}/${id}`, 123 | type: "DELETE", 124 | accepts: { 125 | 'json': "application/json", 126 | 'text': 'text/plain' 127 | } 128 | }) 129 | .then(function (data) { 130 | this.dispatchServerAction(action, kStates.SYNCED, data); 131 | }.bind(this)) 132 | .catch(function (err) { 133 | this.dispatchServerAction(action, kStates.ERRORED, err); 134 | }.bind(this)); 135 | 136 | this.dispatchServerAction(action, kStates.DELETING, {id: id}); 137 | 138 | } 139 | 140 | 141 | /** 142 | * 143 | * Subscription methods 144 | * 145 | */ 146 | _onPostEvent(event, channel, data) { 147 | this.dispatchServerAction(this._actionForMethod('POST'), kStates.SYNCED, { 148 | subscription: this._normalizeChannelName(channel), 149 | id: data.id, 150 | data: data 151 | }); 152 | } 153 | 154 | _onPutEvent(event, channel, data) { 155 | this.dispatchServerAction(this._actionForMethod('PUT'), kStates.SYNCED, { 156 | subscription: this._normalizeChannelName(channel), 157 | id: data.id, 158 | data: data 159 | }); 160 | } 161 | 162 | _onDeleteEvent(event, channel) { 163 | var re = new RegExp(`${this.baseURL}/(.+)$`), // re for extracing resource id from channel 164 | id = re.exec(channel)[1]; 165 | 166 | this.dispatchServerAction(this._actionForMethod('DELETE'), kStates.SYNCED, { 167 | subscription: this._normalizeChannelName(channel), 168 | id: id 169 | }); 170 | } 171 | 172 | subscribeList () { 173 | this._subscribe(this.baseURL, ['POST'], this._onPostEvent); 174 | } 175 | 176 | unsubscribeList () { 177 | this._unsubscribe(this.baseURL, ['POST'], this._onPostEvent); 178 | } 179 | 180 | subscribeResources(ids) { 181 | if (!_.isArray(ids)) { 182 | ids = [ids]; 183 | } 184 | 185 | var channels = _.map(ids, id => `${this.baseURL}/${id}`); 186 | this._subscribe(channels, ['PUT'], this._onPutEvent); 187 | this._subscribe(channels, ['DELETE'], this._onDeleteEvent); 188 | } 189 | 190 | unsubscribeResources(ids) { 191 | if (!_.isArray(ids)) { 192 | ids = [ids]; 193 | } 194 | 195 | var channels = _.map(ids, id => `${this.baseURL}/${id}`); 196 | this._unsubscribe(channels, ['PUT'], this._onPutEvent); 197 | this._unsubscribe(channels, ['DELETE'], this._onDeleteEvent); 198 | } 199 | 200 | } 201 | 202 | module.exports = CRUDBase; 203 | -------------------------------------------------------------------------------- /pubsub-bridge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var url = require('url'), 6 | http = require('http'), 7 | //fernet = require('fernet'), 8 | socketio = require('socket.io'); 9 | 10 | /* 11 | var _fernet = new fernet({ttl: 0}); 12 | 13 | if (!process.env.FERNET_KEY) { 14 | throw "FERNET_KEY missing"; 15 | } 16 | var secret = new fernet.Secret(process.env.FERNET_KEY); 17 | 18 | function fernetDecode(ciphertext) { 19 | if (!ciphertext) { 20 | throw "Cannot Fernet decode, ciphertext is null"; 21 | } 22 | var token = new _fernet.Token({ 23 | secret: secret, 24 | token: ciphertext 25 | }); 26 | return token.decode(); 27 | } 28 | */ 29 | 30 | // auth: parsedUrl.auth ? parsedUrl.auth.split(':')[1] : null 31 | // redis://:@localhost:6379/ 32 | 33 | /* 34 | var parsedUrl = url.parse(process.env.REDIS_URL || process.env.REDISGREEN_URL); 35 | 36 | var redis = require("redis"); 37 | */ 38 | 39 | var _subscriptions = {}; 40 | 41 | module.exports = function (server) { 42 | 43 | var io = socketio(server); 44 | //var redisClient = redis.createClient(parsedUrl.port || 6379, parsedUrl.hostname || 'localhost'); 45 | 46 | function unsubscribe(socket, channel) { 47 | console.log('unsubscribing from ' + channel); 48 | // Get listeners for channel. Remove socket for list. If 49 | // channel listeners becomes empty then unsubscribe from redis 50 | // channel and remove channel from internal subsciptions hash 51 | var channelListeners = _subscriptions[channel]; 52 | if (channelListeners) { 53 | channelListeners.splice(_.indexOf(channelListeners, socket), 1); 54 | if (channelListeners.length === 0) { 55 | //redisClient.unsubscribe(channel); 56 | delete _subscriptions[channel]; 57 | } 58 | } 59 | } 60 | 61 | /* 62 | redisClient.auth(parsedUrl.auth ? parsedUrl.auth.split(':')[1] : null, function () { 63 | console.log('redisClient.auth callback'); 64 | }); 65 | 66 | redisClient.on("error", function (err) { 67 | console.error("Error " + err); 68 | }); 69 | 70 | redisClient.on("ready", function () { 71 | console.log('redis client: ready'); 72 | }); 73 | 74 | redisClient.on("connect", function () { 75 | console.log('redis client: connect'); 76 | }); 77 | 78 | redisClient.on("end", function () { 79 | console.log('redis client: end'); 80 | }); 81 | 82 | redisClient.on("drain", function () { 83 | console.log('redis client: drain'); 84 | }); 85 | 86 | redisClient.on("idle", function () { 87 | console.log('redis client: idle'); 88 | }); 89 | 90 | redisClient.on("message", function (channel, message) { 91 | //message = fernetDecode(message); 92 | var channelListeners = _subscriptions[channel] || []; 93 | if (channelListeners.length === 0) { console.warn('no listeners for channel: ' + channel); } 94 | _.each(channelListeners, function (socket) { 95 | socket.emit('set', {channel: channel, data: message}); 96 | }); 97 | }); 98 | */ 99 | 100 | io.on('connection', function(socket) { 101 | 102 | var interval; 103 | 104 | console.log('socket connection established'); 105 | 106 | socket.on('disconnect', function() { 107 | console.log('socket disconnected - cleanup'); 108 | // remove socket from any subscriptions 109 | _.each(_subscriptions, function(sockets, channel){ 110 | if (_.indexOf(sockets, socket) !== -1) { 111 | unsubscribe(socket, channel); 112 | } 113 | }); 114 | }); 115 | 116 | socket.on('auth', function(accessToken) { 117 | console.log('socket auth set'); 118 | socket.accessToken = accessToken; 119 | }); 120 | 121 | socket.on('subscribe', function (channel, withResponse) { 122 | 123 | /* 124 | * ??? ONLY FOR EXAMPLE to simulate subscription to /api/servertime 125 | * To use redis and remove this HARDCODED /api/servertime example 126 | * remove the following lines through the return 127 | * 128 | */ 129 | 130 | if (channel !== 'PUT /api/servertime') { 131 | return; 132 | } 133 | 134 | console.log('initiate subscription to %s', channel); 135 | 136 | // Get or create listeners list for channel 137 | var channelListeners = _subscriptions[channel] || (_subscriptions[channel] = []); 138 | 139 | // Only add client socket once to channel listeners 140 | if (_.indexOf(channelListeners, socket) !== -1) { 141 | console.error('Attempt to subscribe to channel already subscribed to - channel: %s, socket: %j', channel, socket); 142 | return; 143 | } 144 | channelListeners.push(socket); 145 | 146 | socket.emit('set', {channel: channel, data: JSON.stringify(new Date().toString())}); 147 | console.log('refreshing on subscribe: ' + channel); 148 | console.log('subscribed to ' + channel); 149 | 150 | interval = setInterval(function () { 151 | socket.emit('set', {channel: channel, data: JSON.stringify(new Date().toString())}); 152 | }, 1000); 153 | 154 | return; 155 | 156 | /* 157 | * ??? FOR REDIS below only pertains to redis pubsub bridge. If you use the redis 158 | * remove everything above and use the impl below 159 | */ 160 | 161 | if (!socket.accessToken) { 162 | console.log('cannot subscribe to a channel without an access token'); 163 | return; 164 | } 165 | 166 | console.log('initiate subscription to %s', channel); 167 | 168 | var options = { 169 | hostname: 'localhost', 170 | port: process.env.DJANGO_PORT, 171 | path: channel.replace(/^(GET|PUT|POST|DELETE)\s/, ''), 172 | headers: { 173 | Authorization: "Heroku " + socket.accessToken, 174 | 'X-Forwarded-Proto': 'https' 175 | } 176 | }; 177 | 178 | var successHandler; 179 | 180 | if (withResponse) { 181 | options.method = 'GET'; 182 | successHandler = function(body) { 183 | socket.emit('set', {channel: channel, data: body}); 184 | console.log('refreshing on subscribe: ' + channel); 185 | console.log('subscribed to ' + channel); 186 | }; 187 | } 188 | else { 189 | options.method = 'HEAD'; 190 | successHandler = function() { 191 | console.log('subscribed to ' + channel); 192 | }; 193 | } 194 | 195 | var req = http.request(options, function(res) { 196 | if (res.statusCode === 200) { 197 | 198 | // Get or create listeners list for channel 199 | var channelListeners = _subscriptions[channel] || (_subscriptions[channel] = []); 200 | // If no listeners exist, subscribe to redis channel 201 | if (channelListeners.length === 0) { 202 | //redisClient.subscribe(channel); 203 | } 204 | // Only add client socket once to channel listeners 205 | if (_.indexOf(channelListeners, socket) !== -1) { 206 | console.error('Attempt to subscribe to channel already subscribed to - channel: %s, socket: %j', channel, socket); 207 | return; 208 | } 209 | channelListeners.push(socket); 210 | 211 | // we have to consume the response data always 212 | // or the 'end' event never fires, and sockets 213 | // will remain tied up in the pool for longer 214 | var body = ''; 215 | res.setEncoding('utf8'); 216 | res.on('data', function(chunk) { 217 | body += chunk; 218 | }); 219 | res.on('end', function() { 220 | successHandler(body); 221 | }); 222 | } else { 223 | console.log('failed subscribing to %s; statusCode: %d', channel, res.statusCode); 224 | } 225 | }); 226 | req.on('error', function(e) { 227 | console.log('problem with subscription auth request: ' + e.message); 228 | }); 229 | req.end(); 230 | 231 | }); 232 | 233 | socket.on('unsubscribe', function (channel) { 234 | 235 | // ??? remove if using redis 236 | if (channel !== 'PUT /api/servertime') { 237 | return; 238 | } 239 | 240 | unsubscribe(socket, channel); 241 | 242 | /* 243 | * ??? ONLY FOR EXAMPLE to simulate subscription to /api/servertime 244 | * remove if using redis 245 | */ 246 | clearInterval(interval); 247 | }); 248 | 249 | }); 250 | 251 | return io; 252 | }; 253 | -------------------------------------------------------------------------------- /public/javascripts/vendor/flux/Dispatcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @providesModule Dispatcher 10 | * @typechecks 11 | */ 12 | 13 | var invariant = require('./invariant'); 14 | 15 | var _lastID = 1; 16 | var _prefix = 'ID_'; 17 | 18 | /** 19 | * Dispatcher is used to broadcast payloads to registered callbacks. This is 20 | * different from generic pub-sub systems in two ways: 21 | * 22 | * 1) Callbacks are not subscribed to particular events. Every payload is 23 | * dispatched to every registered callback. 24 | * 2) Callbacks can be deferred in whole or part until other callbacks have 25 | * been executed. 26 | * 27 | * For example, consider this hypothetical flight destination form, which 28 | * selects a default city when a country is selected: 29 | * 30 | * var flightDispatcher = new Dispatcher(); 31 | * 32 | * // Keeps track of which country is selected 33 | * var CountryStore = {country: null}; 34 | * 35 | * // Keeps track of which city is selected 36 | * var CityStore = {city: null}; 37 | * 38 | * // Keeps track of the base flight price of the selected city 39 | * var FlightPriceStore = {price: null} 40 | * 41 | * When a user changes the selected city, we dispatch the payload: 42 | * 43 | * flightDispatcher.dispatch({ 44 | * actionType: 'city-update', 45 | * selectedCity: 'paris' 46 | * }); 47 | * 48 | * This payload is digested by `CityStore`: 49 | * 50 | * flightDispatcher.register(function(payload)) { 51 | * if (payload.actionType === 'city-update') { 52 | * CityStore.city = payload.selectedCity; 53 | * } 54 | * }); 55 | * 56 | * When the user selects a country, we dispatch the payload: 57 | * 58 | * flightDispatcher.dispatch({ 59 | * actionType: 'country-update', 60 | * selectedCountry: 'australia' 61 | * }); 62 | * 63 | * This payload is digested by both stores: 64 | * 65 | * CountryStore.dispatchToken = flightDispatcher.register(function(payload) { 66 | * if (payload.actionType === 'country-update') { 67 | * CountryStore.country = payload.selectedCountry; 68 | * } 69 | * }); 70 | * 71 | * When the callback to update `CountryStore` is registered, we save a reference 72 | * to the returned token. Using this token with `waitFor()`, we can guarantee 73 | * that `CountryStore` is updated before the callback that updates `CityStore` 74 | * needs to query its data. 75 | * 76 | * CityStore.dispatchToken = flightDispatcher.register(function(payload) { 77 | * if (payload.actionType === 'country-update') { 78 | * // `CountryStore.country` may not be updated. 79 | * flightDispatcher.waitFor([CountryStore.dispatchToken]); 80 | * // `CountryStore.country` is now guaranteed to be updated. 81 | * 82 | * // Select the default city for the new country 83 | * CityStore.city = getDefaultCityForCountry(CountryStore.country); 84 | * } 85 | * }); 86 | * 87 | * The usage of `waitFor()` can be chained, for example: 88 | * 89 | * FlightPriceStore.dispatchToken = 90 | * flightDispatcher.register(function(payload)) { 91 | * switch (payload.actionType) { 92 | * case 'country-update': 93 | * flightDispatcher.waitFor([CityStore.dispatchToken]); 94 | * FlightPriceStore.price = 95 | * getFlightPriceStore(CountryStore.country, CityStore.city); 96 | * break; 97 | * 98 | * case 'city-update': 99 | * FlightPriceStore.price = 100 | * FlightPriceStore(CountryStore.country, CityStore.city); 101 | * break; 102 | * } 103 | * }); 104 | * 105 | * The `country-update` payload will be guaranteed to invoke the stores' 106 | * registered callbacks in order: `CountryStore`, `CityStore`, then 107 | * `FlightPriceStore`. 108 | */ 109 | 110 | function Dispatcher() {"use strict"; 111 | this.$Dispatcher_callbacks = {}; 112 | this.$Dispatcher_isPending = {}; 113 | this.$Dispatcher_isHandled = {}; 114 | this.$Dispatcher_isDispatching = false; 115 | this.$Dispatcher_pendingPayload = null; 116 | } 117 | 118 | /** 119 | * Registers a callback to be invoked with every dispatched payload. Returns 120 | * a token that can be used with `waitFor()`. 121 | * 122 | * @param {function} callback 123 | * @return {string} 124 | */ 125 | Dispatcher.prototype.register=function(callback) {"use strict"; 126 | var id = _prefix + _lastID++; 127 | this.$Dispatcher_callbacks[id] = callback; 128 | return id; 129 | }; 130 | 131 | /** 132 | * Removes a callback based on its token. 133 | * 134 | * @param {string} id 135 | */ 136 | Dispatcher.prototype.unregister=function(id) {"use strict"; 137 | invariant( 138 | this.$Dispatcher_callbacks[id], 139 | 'Dispatcher.unregister(...): `%s` does not map to a registered callback.', 140 | id 141 | ); 142 | delete this.$Dispatcher_callbacks[id]; 143 | }; 144 | 145 | /** 146 | * Waits for the callbacks specified to be invoked before continuing execution 147 | * of the current callback. This method should only be used by a callback in 148 | * response to a dispatched payload. 149 | * 150 | * @param {array} ids 151 | */ 152 | Dispatcher.prototype.waitFor=function(ids) {"use strict"; 153 | invariant( 154 | this.$Dispatcher_isDispatching, 155 | 'Dispatcher.waitFor(...): Must be invoked while dispatching.' 156 | ); 157 | for (var ii = 0; ii < ids.length; ii++) { 158 | var id = ids[ii]; 159 | if (this.$Dispatcher_isPending[id]) { 160 | invariant( 161 | this.$Dispatcher_isHandled[id], 162 | 'Dispatcher.waitFor(...): Circular dependency detected while ' + 163 | 'waiting for `%s`.', 164 | id 165 | ); 166 | continue; 167 | } 168 | invariant( 169 | this.$Dispatcher_callbacks[id], 170 | 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.', 171 | id 172 | ); 173 | this.$Dispatcher_invokeCallback(id); 174 | } 175 | }; 176 | 177 | /** 178 | * Dispatches a payload to all registered callbacks. 179 | * 180 | * @param {object} payload 181 | */ 182 | Dispatcher.prototype.dispatch=function(payload) {"use strict"; 183 | invariant( 184 | !this.$Dispatcher_isDispatching, 185 | 'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.' 186 | ); 187 | this.$Dispatcher_startDispatching(payload); 188 | try { 189 | for (var id in this.$Dispatcher_callbacks) { 190 | if (this.$Dispatcher_isPending[id]) { 191 | continue; 192 | } 193 | this.$Dispatcher_invokeCallback(id); 194 | } 195 | } finally { 196 | this.$Dispatcher_stopDispatching(); 197 | } 198 | }; 199 | 200 | /** 201 | * Is this Dispatcher currently dispatching. 202 | * 203 | * @return {boolean} 204 | */ 205 | Dispatcher.prototype.isDispatching=function() {"use strict"; 206 | return this.$Dispatcher_isDispatching; 207 | }; 208 | 209 | /** 210 | * Call the callback stored with the given id. Also do some internal 211 | * bookkeeping. 212 | * 213 | * @param {string} id 214 | * @internal 215 | */ 216 | Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {"use strict"; 217 | this.$Dispatcher_isPending[id] = true; 218 | this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload); 219 | this.$Dispatcher_isHandled[id] = true; 220 | }; 221 | 222 | /** 223 | * Set up bookkeeping needed when dispatching. 224 | * 225 | * @param {object} payload 226 | * @internal 227 | */ 228 | Dispatcher.prototype.$Dispatcher_startDispatching=function(payload) {"use strict"; 229 | for (var id in this.$Dispatcher_callbacks) { 230 | this.$Dispatcher_isPending[id] = false; 231 | this.$Dispatcher_isHandled[id] = false; 232 | } 233 | this.$Dispatcher_pendingPayload = payload; 234 | this.$Dispatcher_isDispatching = true; 235 | }; 236 | 237 | /** 238 | * Clear bookkeeping used for dispatching. 239 | * 240 | * @internal 241 | */ 242 | Dispatcher.prototype.$Dispatcher_stopDispatching=function() {"use strict"; 243 | this.$Dispatcher_pendingPayload = null; 244 | this.$Dispatcher_isDispatching = false; 245 | }; 246 | 247 | 248 | module.exports = Dispatcher; 249 | -------------------------------------------------------------------------------- /public/fonts/bootstrap/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------