├── .babelrc
├── .eslintrc
├── .gitignore
├── README.md
├── package.json
├── public
├── build.html
└── index.html
├── src
├── Root.js
├── actions
│ ├── FunFairShiftActions.js
│ ├── GenericActions.js
│ └── SemaphoreActions.js
├── components
│ ├── Comment.js
│ ├── Comments.js
│ ├── FunFairShift.js
│ ├── FunFairShifts.js
│ ├── Semaphore.js
│ ├── SemaphoreStateForm.js
│ └── Semaphores.js
├── constants
│ ├── ActionTypes.js
│ └── index.js
├── container
│ └── FunFairShiftsApp.js
├── index.js
├── middleware
│ ├── api.js
│ └── throttle.js
├── reducers
│ ├── funFairShifts.js
│ ├── index.js
│ └── semaphores.js
├── scss
│ └── stylesheet.scss
└── util
│ ├── helpers.js
│ └── templateHelpers.js
├── stub
├── semaphores.json
├── shifts.json
└── state.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "plugins": ["lodash"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "es6": true
6 | },
7 | "ecmaFeatures": {
8 | "modules": true,
9 | "jsx": true
10 | },
11 | "rules": {
12 | "strict": 0,
13 | "semi": [2, "always"]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | .idea
3 | *.tar
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Redux + React-Router demo
2 |
3 | ## Introduction
4 |
5 | Since I got excited about the React + Redux combo, I've tried it out on a current project. It was working well for me,
6 | so I've extracted relevant parts out into this demo.
7 |
8 | This demo application represents a "fun fair" (amusement park) with two daily shifts (morning and afternoon), while each
9 | shift has a group of semaphores to indicate the queue length (of people waiting for the attraction).
10 | The semaphores are updated automatically server-side (just polling every 5 seconds in this demo), but employees can
11 | also change the state of a semaphore by clicking on it and submitting the form.
12 |
13 | ## JS stack
14 |
15 | * ES6, Webpack, Babel
16 | * React 0.14.0-beta1 + Redux 1.0.0 + React-router 1.0.0-beta3
17 | * [fetch](https://github.com/github/fetch)
18 | * [classnames](https://github.com/JedWatson/classnames)
19 | * Lodash + [babel-plugin-lodash](https://github.com/megawac/babel-plugin-lodash)
20 |
21 | ## CSS stack
22 |
23 | * SASS (node-sass)
24 | * PureCSS
25 | * PostCSS + Autoprefixer
26 |
27 | ## Features
28 |
29 | * Async actions (i.e. XHRs)
30 | * Middleware
31 | * [redux-thunk](https://github.com/gaearon/redux-thunk)
32 | * [redux-logger](https://github.com/fcomb/redux-logger)
33 | * Custom "api" middleware for XHRs
34 | * Custom "throttle" middleware
35 | * Basic development/production toggle
36 | * `__DEV__`: react-hot-loader, logger middleware
37 | * Non-`__DEV__`: optimize with UglifyJS
38 | * CSS is compiled separately
39 |
40 | ## Development
41 |
42 | npm install
43 | npm run dev
44 |
45 | Go to [http://localhost:8080/public](http://localhost:8080/public).
46 |
47 | ## Production build
48 |
49 | npm run build
50 |
51 | Find build at `./build/bundle.js`.
52 |
53 | ## TODO
54 |
55 | * Improve on applying Redux architecture
56 | * Better error handling
57 | * Input validation
58 | * Better config options
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "FunFair",
3 | "version": "1.0.0",
4 | "main": "src/index.js",
5 | "scripts": {
6 | "build": "mkdir -p build/css && node-sass src/scss/stylesheet.scss --output-style=compressed | postcss --use autoprefixer -o build/css/stylesheet.css && BUILD_RELEASE=1 webpack -p",
7 | "dev": "node-sass -rw src/scss src/scss/stylesheet.scss -o build/css | postcss --use autoprefixer -o build/css/stylesheet.css & webpack-dev-server",
8 | "postbuild": "cp -R node_modules/font-awesome/fonts build/; cp public/build.html build/index.html",
9 | "postinstall": "npm run build",
10 | "lint": "eslint src"
11 | },
12 | "dependencies": {
13 | "classnames": "^2.1.3",
14 | "font-awesome": "^4.4.0",
15 | "lodash": "^3.10.1",
16 | "purecss": "^0.6.0",
17 | "react": "0.14.0-beta1",
18 | "react-redux": "^0.8.2",
19 | "react-router": "1.0.0-beta3",
20 | "redux": "^1.0.0",
21 | "redux-logger": "0.0.3",
22 | "redux-thunk": "^0.1.0",
23 | "whatwg-fetch": "^0.9.0"
24 | },
25 | "devDependencies": {
26 | "autoprefixer": "^5.2.0",
27 | "babel-core": "^5.8.20",
28 | "babel-eslint": "^4.0.5",
29 | "babel-loader": "^5.3.2",
30 | "babel-plugin-lodash": "^0.1.5",
31 | "eslint": "^1.1.0",
32 | "extract-text-webpack-plugin": "^0.8.2",
33 | "imports-loader": "^0.6.4",
34 | "node-libs-browser": "^0.5.2",
35 | "node-sass": "^3.2.0",
36 | "postcss-cli": "^1.5.0",
37 | "react": "^0.13.3",
38 | "react-hot-api": "0.4.5",
39 | "react-hot-loader": "^1.2.8",
40 | "webpack": "^1.10.5",
41 | "webpack-dev-server": "^1.10.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/build.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fun Fair
6 |
7 |
8 |
9 |
10 |
11 |
12 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fun Fair
6 |
7 |
8 |
9 |
10 |
11 |
12 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/Root.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Redirect, Router, Route } from 'react-router';
3 | import { createStore, combineReducers, applyMiddleware } from 'redux';
4 | import { Provider } from 'react-redux';
5 | import thunk from 'redux-thunk';
6 | import logger from 'redux-logger';
7 | import api from './middleware/api';
8 | import throttle from './middleware/throttle';
9 | import * as reducers from './reducers';
10 | import FunFairShiftsApp from './container/FunFairShiftsApp';
11 | import FunFairShifts from './components/FunFairShifts';
12 | import Semaphores from './components/Semaphores';
13 |
14 | let middleware = [thunk, throttle, api];
15 | if(__DEV__) middleware.push(logger);
16 |
17 | const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore),
18 | reducer = combineReducers(reducers),
19 | store = createStoreWithMiddleware(reducer);
20 |
21 | export default class Root extends Component {
22 | static propTypes = {
23 | history: PropTypes.object.isRequired
24 | };
25 |
26 | render() {
27 | const { history } = this.props;
28 | return (
29 |
30 | {renderRoutes.bind(null, history)}
31 |
32 | );
33 | }
34 | }
35 |
36 | function renderRoutes(history) {
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/actions/FunFairShiftActions.js:
--------------------------------------------------------------------------------
1 | import { FUNFAIRSHIFTS_REQUEST, FUNFAIRSHIFTS_SELECT } from '../constants/ActionTypes';
2 |
3 | function fetchShifts() {
4 | return {
5 | type: FUNFAIRSHIFTS_REQUEST,
6 | payload: {
7 | url: FUNFAIR_CONFIG.API.current['shifts']
8 | },
9 | meta: {
10 | throttle: 2000
11 | }
12 | }
13 | }
14 |
15 | export function loadShifts() {
16 | return dispatch => {
17 | return dispatch(fetchShifts());
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/actions/GenericActions.js:
--------------------------------------------------------------------------------
1 | import { FUNFAIRSHIFTS_SELECT } from '../constants/ActionTypes';
2 |
3 | export default {
4 | setSelectedShift(shiftId) {
5 | return {
6 | type: FUNFAIRSHIFTS_SELECT,
7 | payload: {
8 | shiftId: shiftId
9 | }
10 | };
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/actions/SemaphoreActions.js:
--------------------------------------------------------------------------------
1 | import { SEMAPHORES_REQUEST, SEMAPHORE_HOVER, SEMAPHORE_LOCK, SEMAPHORE_STATE_SUBMIT } from '../constants/ActionTypes.js';
2 |
3 | function fetchSemaphores(getState) {
4 |
5 | let state = getState(),
6 | { selectedShiftId } = state.funFairShifts;
7 |
8 | return {
9 | type: SEMAPHORES_REQUEST,
10 | payload: {
11 | url: `${FUNFAIR_CONFIG.API.current['semaphores']}/${selectedShiftId}`
12 | },
13 | meta: {
14 | throttle: 2000
15 | }
16 | }
17 | }
18 |
19 | function storeSemaphoreState(data) {
20 |
21 | let url = `${FUNFAIR_CONFIG.API.current['state']}/${data.SHIFT_ID}/${data.SEMAPHORE_ID}`;
22 |
23 | return {
24 | type: SEMAPHORE_STATE_SUBMIT,
25 | payload: {
26 | url: url,
27 | method: url.indexOf('stub') === -1 ? 'POST' : 'GET',
28 | data: data
29 | }
30 | }
31 | }
32 |
33 | export default {
34 |
35 | loadSemaphores() {
36 | return (dispatch, getState) => {
37 | return dispatch(fetchSemaphores(getState));
38 | };
39 | },
40 |
41 | selectSemaphore(semaphoreId) {
42 | return {
43 | type: SEMAPHORE_HOVER,
44 | payload: {
45 | semaphoreId: semaphoreId
46 | }
47 | };
48 | },
49 |
50 | lockSemaphore(semaphoreId, requestState) {
51 | return {
52 | type: SEMAPHORE_LOCK,
53 | payload: {
54 | semaphoreId: semaphoreId,
55 | semaphoreState: requestState
56 | }
57 | };
58 | },
59 |
60 | saveSemaphoreState(data) {
61 | return dispatch => {
62 | return dispatch(storeSemaphoreState(data));
63 | };
64 | }
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/Comment.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import { formatTime } from '../util/templateHelpers';
4 | import { SEMAPHORE_MAP } from '../constants';
5 |
6 | export default class Comment extends React.Component {
7 |
8 | render() {
9 |
10 | let comment = this.props.comment,
11 | classes = classNames('comment-state', SEMAPHORE_MAP[comment.STATE]);
12 |
13 | return (
14 | {formatTime(comment.TIMESTAMP)} {comment.REASON} - {comment.ACTION}
15 | );
16 | }
17 |
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Comments.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Comment from './Comment';
3 | import { connect } from 'react-redux';
4 |
5 | export default class Comments extends React.Component {
6 |
7 | render() {
8 |
9 | let semaphore = this.props.semaphore;
10 |
11 | if(!semaphore) return null;
12 |
13 | let comments = semaphore.STATES.map((comment, i) => );
14 |
15 | return (
16 | {comments}
17 | );
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/FunFairShift.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import { Link } from 'react-router';
4 | import { formatDate, formatTime } from '../util/templateHelpers';
5 | import { STATUS_MAP, CLASSNAME_HIGHLIGHT } from '../constants';
6 |
7 | let ReactPropTypes = React.PropTypes;
8 |
9 | const today = formatDate(new Date().toISOString());
10 |
11 | export default class Shift extends React.Component {
12 |
13 | render() {
14 |
15 | let shift = this.props.shift;
16 |
17 | let classes = {
18 | today: today === formatDate(shift.START) ? CLASSNAME_HIGHLIGHT : ''
19 | };
20 |
21 | return (
22 |
23 | {shift.ID} |
24 | {formatDate(shift.START)} |
25 | {shift.TYPE} |
26 | {formatTime(shift.START)} |
27 | {formatTime(shift.END)} |
28 |
29 | );
30 | }
31 | };
32 |
33 | Shift.propTypes = {
34 | shift: ReactPropTypes.object.isRequired
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/FunFairShifts.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router';
4 | import { loadShifts } from '../actions/FunFairShiftActions';
5 | import FunFairShift from './FunFairShift';
6 | import { FUNFAIRSHIFTS_SORT } from '../constants/ActionTypes';
7 | import _ from 'lodash';
8 |
9 | @connect(state => state.funFairShifts)
10 | export default class FunFairShifts extends React.Component {
11 |
12 | componentWillMount() {
13 | this.props.dispatch(loadShifts());
14 | }
15 |
16 | componentWillReceiveProps(nextProps) {
17 | let sortKey = _.get(nextProps, 'location.query.sort');
18 | if(sortKey === nextProps.sortKey) return;
19 | this.props.dispatch({
20 | type: FUNFAIRSHIFTS_SORT,
21 | payload: {
22 | sortKey: sortKey
23 | }
24 | });
25 | }
26 |
27 | getSortableKeys(shift) {
28 | return _.mapValues(shift, (value, key) => key === this.props.sortKey ? '-' + key : key);
29 | }
30 |
31 | render() {
32 |
33 | let shifts = this.props.shifts;
34 |
35 | if(shifts.length === 0) return null;
36 |
37 | let shiftComponents = shifts.map(shift => );
38 |
39 | let sort = this.getSortableKeys(shifts[0]);
40 |
41 | return (
42 |
43 |
44 |
45 |
46 | ID |
47 | DATE |
48 | TYPE |
49 | START |
50 | END |
51 |
52 |
53 |
54 | {shiftComponents}
55 |
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/Semaphore.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import SemaphoreActions from '../actions/SemaphoreActions';
4 | import { getCurrentState } from '../reducers/semaphores';
5 | import classNames from 'classnames';
6 | import { formatTime } from '../util/templateHelpers';
7 | import { SEMAPHORE_MAP, INDICATOR_SHORT, INDICATOR_MEDIUM, INDICATOR_LONG, CLASSNAME_SHORT, CLASSNAME_MEDIUM, CLASSNAME_LONG } from '../constants';
8 |
9 | @connect((state, props) => {
10 | return {
11 | semaphore: state.semaphores.semaphores[props.id],
12 | lockedSemaphoreId: state.semaphores.lockedSemaphoreId,
13 | lockedSemaphoreState: state.semaphores.lockedSemaphoreState
14 | };
15 | })
16 | export default class Semaphore extends React.Component {
17 |
18 | handleMouseEnter() {
19 | this.props.dispatch(SemaphoreActions.selectSemaphore(this.props.id));
20 | }
21 |
22 | handleClick(evt) {
23 | this.props.dispatch(SemaphoreActions.lockSemaphore(this.props.id, evt.target.getAttribute('data-state')));
24 | }
25 |
26 | render() {
27 |
28 | const { semaphore, lockedSemaphoreId, lockedSemaphoreState } = this.props;
29 |
30 | if(!semaphore) {
31 | return ();
32 | }
33 |
34 | let classes = classNames(['semaphore', SEMAPHORE_MAP[getCurrentState(semaphore)], SEMAPHORE_MAP[semaphore.ID === lockedSemaphoreId ? lockedSemaphoreState : null]]);
35 |
36 | let lineClass = this.props.lines ? ['line', ...this.props.lines.split(' ').map(l => `line-${l}`)].join(' ') : '';
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
{semaphore.TITLE}
{formatTime(this.props.tobt)}
46 |
47 | );
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/SemaphoreStateForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { saveSemaphoreState } from '../actions/SemaphoreActions';
4 | import Comments from './Comments';
5 |
6 | function validate(data) {
7 | let invalid = {};
8 | if(data.SEMAPHORE_REASON === undefined || data.SEMAPHORE_REASON === '') {
9 | invalid.reason = true;
10 | }
11 | if(data.SEMAPHORE_ACTION === undefined || data.SEMAPHORE_ACTION === '') {
12 | invalid.action = true;
13 | }
14 | return Object.keys(invalid).length === 0 ? false : invalid;
15 | }
16 |
17 | @connect(state => state)
18 | export default class SemaphoreStateForm extends React.Component {
19 |
20 | handleInput() {
21 |
22 | var data = {
23 | SHIFT_ID: this.props.funFairShifts.selectedShiftId,
24 | SEMAPHORE_ID: this.props.semaphores.lockedSemaphoreId,
25 | SEMAPHORE_REASON: this.refs.reason.getDOMNode().value,
26 | SEMAPHORE_ACTION: this.refs.action.getDOMNode().value,
27 | SEMAPHORE_STATE: this.props.semaphores.lockedSemaphoreState
28 | };
29 |
30 | let invalid = validate(data);
31 |
32 | if(invalid) {
33 | (this.refs.reason.getDOMNode()).classList[invalid.reason ? 'add' : 'remove']('invalid');
34 | (this.refs.action.getDOMNode()).classList[invalid.action ? 'add' : 'remove']('invalid');
35 | }
36 |
37 | return [data, invalid];
38 | }
39 |
40 | handleSend(evt) {
41 | evt.preventDefault();
42 |
43 | let [data, invalid] = this.handleInput();
44 |
45 | if(!invalid) {
46 | this.props.dispatch(saveSemaphoreState(data));
47 | this.refs.submitButton.getDOMNode().disabled = true;
48 | }
49 | }
50 |
51 | shouldComponentUpdate(nextProps, nextState) {
52 | if(this.props.semaphores.hoveredSemaphoreId !== nextProps.semaphores.hoveredSemaphoreId) return false;
53 | return true;
54 | }
55 |
56 | render() {
57 |
58 | let semaphore = this.props.semaphores.semaphores[this.props.semaphores.lockedSemaphoreId];
59 |
60 | if(!semaphore) return null;
61 |
62 | return (
63 |
64 |
65 |
66 |
{semaphore.TITLE}
67 |
68 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/Semaphores.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import GenericActions from '../actions/GenericActions';
4 | import { loadShifts } from '../actions/FunFairShiftActions';
5 | import { loadSemaphores } from '../actions/SemaphoreActions';
6 | import Semaphore from './Semaphore';
7 | import SemaphoreStateForm from './SemaphoreStateForm';
8 | import Comments from './Comments';
9 | import _ from 'lodash';
10 | import { formatDate, formatTime } from '../util/templateHelpers';
11 |
12 | @connect(state => state)
13 | export default class Semaphores extends React.Component {
14 |
15 | componentWillMount() {
16 | this.props.dispatch(GenericActions.setSelectedShift(this.props.params.SHIFT_ID));
17 | this.props.dispatch(loadShifts());
18 | this.setRefreshInterval();
19 | }
20 |
21 | setRefreshInterval() {
22 | this.clearRefreshInterval();
23 | this._refreshInterval = setInterval(::this.refresh, 60 * 1000);
24 | this.refresh();
25 | }
26 |
27 | clearRefreshInterval() {
28 | clearInterval(this._refreshInterval);
29 | }
30 |
31 | refresh() {
32 | this.props.dispatch(loadSemaphores());
33 | }
34 |
35 | componentWillUnmount() {
36 | clearInterval(this._refreshInterval);
37 | }
38 |
39 | render() {
40 |
41 | let shift = _.find(this.props.funFairShifts.shifts, {ID: this.props.funFairShifts.selectedShiftId}) || {};
42 |
43 | let semaphore = this.props.semaphores.semaphores[this.props.semaphores.hoveredSemaphoreId],
44 | semaphoreTitle = semaphore ? ({semaphore.TITLE}
) : null;
45 |
46 | let line = 'line line-horizontal';
47 |
48 | return (
49 |
50 |
51 |
{formatDate(shift.START)} {shift.TYPE} ({formatTime(shift.START)}-{formatTime(shift.END)})
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 | {semaphoreTitle}
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const FUNFAIRSHIFTS_REQUEST = 'FUNFAIRSHIFTS_REQUEST';
2 | export const FUNFAIRSHIFTS_SORT = 'FUNFAIRSHIFTS_SORT';
3 | export const FUNFAIRSHIFTS_SELECT = 'FUNFAIRSHIFTS_SELECT';
4 | export const SEMAPHORES_REQUEST = 'SEMAPHORES_REQUEST';
5 | export const SEMAPHORE_HOVER = 'SEMAPHORE_HOVER';
6 | export const SEMAPHORE_LOCK = 'SEMAPHORE_LOCK';
7 | export const SEMAPHORE_STATE_SUBMIT = 'SEMAPHORE_STATE_SUBMIT';
8 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export const CLASSNAME_SHORT = 'short';
2 | export const CLASSNAME_MEDIUM = 'medium';
3 | export const CLASSNAME_LONG = 'long';
4 | export const CLASSNAME_HIGHLIGHT = 'highlight';
5 |
6 | export const INDICATOR_SHORT = 'SHORT';
7 | export const INDICATOR_MEDIUM = 'MEDIUM';
8 | export const INDICATOR_LONG = 'LONG';
9 |
10 | export const STATUS_MAP = {};
11 | STATUS_MAP[INDICATOR_SHORT] = CLASSNAME_SHORT;
12 | STATUS_MAP[INDICATOR_MEDIUM] = CLASSNAME_MEDIUM;
13 | STATUS_MAP[INDICATOR_LONG] = CLASSNAME_LONG;
14 |
15 | export const SEMAPHORE_MAP = {};
16 | SEMAPHORE_MAP[INDICATOR_SHORT] = CLASSNAME_SHORT;
17 | SEMAPHORE_MAP[INDICATOR_MEDIUM] = CLASSNAME_MEDIUM;
18 | SEMAPHORE_MAP[INDICATOR_LONG] = CLASSNAME_LONG;
19 |
20 | export const INDICATOR_PRIORITY = [INDICATOR_LONG, INDICATOR_MEDIUM, INDICATOR_SHORT];
21 |
--------------------------------------------------------------------------------
/src/container/FunFairShiftsApp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | @connect(state => state)
5 | export default class FunFairShiftsApp extends React.Component {
6 |
7 | render() {
8 | return (
9 |
10 | {this.props.children}
11 |
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HashHistory from 'react-router/lib/HashHistory';
3 | import Root from './Root';
4 |
5 | let env = __DEV__ ? 'local' : 'production';
6 |
7 | FUNFAIR_CONFIG.API.current = {};
8 | for(let key in FUNFAIR_CONFIG.API[env]) {
9 | FUNFAIR_CONFIG.API.current[key] = FUNFAIR_CONFIG.API[env].host + FUNFAIR_CONFIG.API[env][key];
10 | }
11 |
12 | const history = new HashHistory();
13 |
14 | React.render(, document.getElementById('app'));
15 |
--------------------------------------------------------------------------------
/src/middleware/api.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 | import { getRequestHeaders } from '../util/helpers.js';
3 |
4 | function checkStatus(response) {
5 | if(response.status >= 200 && response.status < 300) {
6 | return response
7 | } else {
8 | var error = new Error(response.statusText);
9 | error.response = response;
10 | throw error;
11 | }
12 | }
13 |
14 | function request({ url, method, data }, successCallback, errorCallback) {
15 |
16 | fetch(url, {
17 | method: method || 'GET',
18 | headers: getRequestHeaders(),
19 | body: data ? JSON.stringify(data) : undefined
20 | })
21 | .then(checkStatus)
22 | .then(response => response.json())
23 | .then(successCallback)
24 | .catch(errorCallback);
25 | }
26 |
27 | export default store => dispatch => action => {
28 |
29 | if(!action.payload || !action.payload.url) {
30 | return dispatch(action);
31 | }
32 |
33 | const { type } = action;
34 | const { url, method, data } = action.payload;
35 |
36 | dispatch({type: type});
37 |
38 | return request({url, method, data}, payload => dispatch({
39 | type: type + '_SUCCESS',
40 | payload
41 | }), err => dispatch({
42 | type: type + '_ERROR',
43 | error: err.message || 'Unknown',
44 | status: (err.response && err.response.status) || 0
45 | }));
46 | };
47 |
--------------------------------------------------------------------------------
/src/middleware/throttle.js:
--------------------------------------------------------------------------------
1 | let cache = {};
2 |
3 | export default store => dispatch => action => {
4 |
5 | if (!action.type || !action.meta || !action.meta.throttle) {
6 | return dispatch(action);
7 | }
8 |
9 | const { type, meta } = action;
10 |
11 | if(cache[type]) {
12 | dispatch({type: type + '_THROTTLED'});
13 | } else {
14 | cache[type] = true;
15 | setTimeout(() => delete cache[type], meta.throttle);
16 | return dispatch(action);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/reducers/funFairShifts.js:
--------------------------------------------------------------------------------
1 | import { FUNFAIRSHIFTS_REQUEST, FUNFAIRSHIFTS_SORT, FUNFAIRSHIFTS_SELECT } from '../constants/ActionTypes';
2 | import _ from 'lodash';
3 |
4 | const initialState = {
5 | shifts: [],
6 | selectedShiftId: null,
7 | sortKey: '-START'
8 | };
9 |
10 | const actionsMap = {
11 | [FUNFAIRSHIFTS_REQUEST + '_SUCCESS']: (state, action) => ({shifts: sortBy(action.payload.SHIFTS, state.sortKey)}),
12 | [FUNFAIRSHIFTS_SORT]: (state, action) => ({shifts: sortBy(state.shifts, action.payload.sortKey), sortKey: action.payload.sortKey}),
13 | [FUNFAIRSHIFTS_SELECT]: (state, action) => ({selectedShiftId: action.payload.shiftId})
14 | };
15 |
16 | const sortBy = (shifts, key) => {
17 | if(!key) return shifts;
18 | return _.sortByOrder(shifts, key.replace(/^-/, ''), key.indexOf('-') !== 0);
19 | };
20 |
21 | export default function shifts(state = initialState, action) {
22 | const reduceFn = actionsMap[action.type];
23 | if(!reduceFn) return state;
24 |
25 | return {...state, ...reduceFn(state, action)};
26 | }
27 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | export funFairShifts from './funFairShifts';
2 | export semaphores from './semaphores';
3 |
--------------------------------------------------------------------------------
/src/reducers/semaphores.js:
--------------------------------------------------------------------------------
1 | import { SEMAPHORES_REQUEST, SEMAPHORE_HOVER, SEMAPHORE_LOCK, SEMAPHORE_STATE_SUBMIT } from '../constants/ActionTypes';
2 | import _ from 'lodash';
3 |
4 | const initialState = {
5 | semaphores: {},
6 | hoveredSemaphoreId: 'entrance',
7 | lockedSemaphoreId: null,
8 | lockedSemaphoreState: null
9 | };
10 |
11 | const actionsMap = {
12 | [SEMAPHORES_REQUEST + '_SUCCESS']: (state, action) => ({semaphores: _.indexBy(action.payload.SEMAPHORES, 'ID')}),
13 | [SEMAPHORE_HOVER]: (state, action) => ({hoveredSemaphoreId: action.payload.semaphoreId}),
14 | [SEMAPHORE_LOCK]: (state, action) => ({lockedSemaphoreId: action.payload.semaphoreId, lockedSemaphoreState: action.payload.semaphoreState}),
15 | [SEMAPHORE_STATE_SUBMIT + '_SUCCESS']: (state, action) => ({lockedSemaphoreId: null, lockedSemaphoreState: null})
16 | };
17 |
18 | const getCurrentState = semaphore => {
19 | let state = _.find(semaphore.STATES, {CURRENT: 'Y'});
20 | return state ? state.STATE : null;
21 | };
22 |
23 | export default function semaphores(state = initialState, action) {
24 | const reduceFn = actionsMap[action.type];
25 | if(!reduceFn) return state;
26 |
27 | return {...state, ...reduceFn(state, action)};
28 | }
29 |
30 | export { getCurrentState };
31 |
--------------------------------------------------------------------------------
/src/scss/stylesheet.scss:
--------------------------------------------------------------------------------
1 | @import "../../node_modules/font-awesome/css/font-awesome";
2 | @import "../../node_modules/purecss/build/pure";
3 |
4 | $white: #FFF;
5 | $black: #000;
6 | $blue: rgb(66, 184, 221);
7 | $changed: $blue;
8 | $short: rgb(28, 184, 65);
9 | $medium: rgb(223, 117, 20);
10 | $long: rgb(202, 60, 60);
11 | $short_inactive: rgba(28, 184, 65, 1);
12 | $medium_inactive: rgba(223, 117, 20, 0.4);
13 | $long_inactive: rgba(202, 60, 60, 0.4);
14 | $highlight: rgb(252, 255, 105);
15 |
16 | * {
17 | box-sizing: border-box;
18 | }
19 |
20 | body {
21 | font-family: "Menlo", Helvetica, sans-serif;
22 | font-size: 16px;
23 | color: #333;
24 | }
25 |
26 | hr {
27 | height: 0;
28 | border: none;
29 | border-bottom: 1px solid #444;
30 | }
31 |
32 | .container {
33 | max-width: 960px;
34 | margin: 0 auto;
35 | }
36 |
37 | .hide {
38 | display: none !important;
39 | }
40 |
41 | .short,
42 | td.short,
43 | td.short a {
44 | background: $short;
45 | color: $white;
46 | }
47 |
48 | .medium,
49 | td.medium,
50 | td.medium a {
51 | background-color: $medium;
52 | color: $white;
53 | }
54 |
55 | .long,
56 | td.long,
57 | td.long a {
58 | background: $long;
59 | color: $white;
60 | }
61 |
62 | .short-text {
63 | color: $short;
64 | }
65 |
66 | .medium-text {
67 | color: $medium;
68 | }
69 |
70 | .long-text {
71 | color: $long;
72 | }
73 |
74 | .highlight {
75 | font-weight: bold;
76 | background-color: $highlight;
77 | }
78 |
79 | #shifts {
80 | margin-top: 1em;
81 | position: relative;
82 | }
83 |
84 | th[data-sort], td {
85 | cursor: pointer;
86 | }
87 |
88 | .hide-rows tbody tr:not(.open) {
89 | display: none;
90 | }
91 |
92 | .details-layer {
93 | padding: 2em 0 0;
94 | }
95 |
96 | .pure-table-minimal {
97 | border-collapse: separate;
98 | border-spacing: 3px;
99 | border: none;
100 | width: 100%;
101 | }
102 |
103 | .pure-table-minimal thead {
104 | background-color: inherit;
105 | }
106 |
107 | .pure-table-minimal tr:hover {
108 | background-color: #EEE;
109 | }
110 |
111 | .pure-table-minimal td,
112 | .pure-table-minimal th {
113 | border: none;
114 | }
115 |
116 | .pure-table-minimal th {
117 | padding-left: 2px;
118 | padding-right: 2px;
119 | }
120 |
121 | .pure-table-minimal td {
122 | padding: 2px;
123 | }
124 |
125 | .pure-table-minimal tbody > tr:last-child > td {
126 | border-bottom-width: 0;
127 | }
128 |
129 | .pure-table-minimal .short a,
130 | .pure-table-minimal .medium a,
131 | .pure-table-minimal .long a {
132 | text-decoration: none;
133 | display: block;
134 | }
135 |
136 | .boarding {
137 | width: 30px;
138 | color: $white;
139 | }
140 |
141 | .pure-button-wrap {
142 | white-space: normal;
143 | margin-top: 4px;
144 | }
145 |
146 | .pure-button-reload {
147 | float: right;
148 | background-color: $blue;
149 | color: $white;
150 | padding: 2px 4px;
151 | border-radius: 50%;
152 | }
153 |
154 | .sort-column {
155 | text-decoration: underline;
156 | }
157 |
158 | .flex-container {
159 | display: flex;
160 | flex-direction: row;
161 | }
162 |
163 | .comments div {
164 | flex: 1;
165 | text-align: center;
166 | &:nth-child(1),
167 | &:nth-child(2) {
168 | flex: 3;
169 | }
170 | }
171 |
172 | .semaphore-group {
173 |
174 | .row {
175 | padding: 0 5px;
176 | width: 100%;
177 |
178 | > div {
179 | flex: 1;
180 | text-align: center;
181 | position: relative;
182 | overflow: hidden;
183 | margin-left: -1px;
184 |
185 | div {
186 | min-height: 3em;
187 | }
188 |
189 | &.line {
190 | &::before {
191 | content: '';
192 | position: absolute;
193 | top: 68px;
194 | left: 0;
195 | border-top: 1px solid #ccc;
196 | height: 0;
197 | z-index: -1;
198 | }
199 | &::after {
200 | content: '';
201 | position: absolute;
202 | border-right: 1px solid #ccc;
203 | width: 0;
204 | z-index: -1;
205 | }
206 | }
207 |
208 | &.line-horizontal {
209 | &::before {
210 | width: 100%;
211 | }
212 | }
213 |
214 | &.line-left {
215 | &::before {
216 | width: 50%;
217 | }
218 | }
219 |
220 | &.line-right {
221 | &::before {
222 | width: 50%;
223 | left: 50%;
224 | }
225 | }
226 |
227 | &.line-upper-left {
228 | &::after {
229 | top: 0;
230 | left: 0;
231 | height: 68px;
232 | }
233 | }
234 | &.line-upper-right {
235 | &::after {
236 | top: 0;
237 | right: 0;
238 | height: 68px;
239 | }
240 | }
241 | &.line-lower-left {
242 | &::after {
243 | top: 68px;
244 | left: 0;
245 | height: 100%;
246 | }
247 | }
248 | &.line-lower-right {
249 | &::after {
250 | top: 68px;
251 | right: 0;
252 | height: 100%;
253 | }
254 | }
255 | &.line-vertical-right {
256 | &::after {
257 | top: 0;
258 | right: 0;
259 | height: 100%;
260 | }
261 | }
262 | &.line-vertical-left {
263 | &::after {
264 | top: 0;
265 | left: 0;
266 | height: 100%;
267 | }
268 | }
269 | }
270 |
271 | .semaphore {
272 |
273 | padding: 10px;
274 | margin: 0 auto;
275 | background: $black;
276 | border-radius: 5px;
277 | cursor: pointer;
278 | max-width: 70px;
279 |
280 | .long:hover,
281 | .medium:hover,
282 | .short:hover {
283 | opacity: 0.7;
284 | }
285 |
286 | &.long .long,
287 | &.medium .medium,
288 | &.short .short{
289 | opacity: 1;
290 | }
291 |
292 | span {
293 | display: block;
294 | width: 2em;
295 | height: 2em;
296 | margin: 5px auto;
297 | border-radius: 50%;
298 | opacity: .4;
299 |
300 | &.long {
301 | background: red;
302 | background-image: radial-gradient(red, transparent);
303 | background-size: 2px 2px;
304 | box-shadow: 0 0 8px #111 inset, 0 0 4px red;
305 | }
306 | &.medium {
307 | background: orange;
308 | background-image: radial-gradient(orange, transparent);
309 | background-size: 2px 2px;
310 | box-shadow: 0 0 8px #111 inset, 0 0 4px orange;
311 | }
312 | &.short {
313 | background: green;
314 | background-image: radial-gradient(lime, transparent);
315 | background-size: 2px 2px;
316 | box-shadow: 0 0 8px #111 inset, 0 0 4px lime;
317 | }
318 |
319 | }
320 |
321 | &:after {
322 | content: "";
323 | display: table;
324 | clear: both;
325 | }
326 |
327 | }
328 |
329 | .row-header {
330 | flex: 0.3;
331 | margin-top: 50px;
332 | font-size: 2em;
333 | }
334 | }
335 |
336 | }
337 |
338 | input,
339 | textarea,
340 | select,
341 | .error-message {
342 | width: 90%;
343 | margin: 3px auto;
344 | }
345 |
346 | input.invalid,
347 | textarea.invalid {
348 | border: 1px solid red;
349 | }
350 |
351 | input + .error-message,
352 | textarea + .error-message {
353 | text-align: left;
354 | color: red;
355 | display: none;
356 | }
357 |
358 | input.invalid + .error-message,
359 | textarea.invalid + .error-message {
360 | display: block;
361 | }
362 |
363 | .comment-state {
364 | display: inline-block;
365 | width: 12px;
366 | height: 12px;
367 | border-radius: 50%;
368 | }
369 |
--------------------------------------------------------------------------------
/src/util/helpers.js:
--------------------------------------------------------------------------------
1 | const token = btoa('username' + ':' + 'password');
2 |
3 | export function getRequestHeaders() {
4 | return {
5 | Authorization: 'Basic ' + token,
6 | Accept: 'application/json'
7 | };
8 | }
9 |
10 | export function setRequestHeaders(xhr) {
11 | xhr.setRequestHeader('Authorization', 'Basic ' + token);
12 | xhr.setRequestHeader('Accept', 'application/json');
13 | }
14 |
--------------------------------------------------------------------------------
/src/util/templateHelpers.js:
--------------------------------------------------------------------------------
1 | function pad(num) {
2 | return ('0' + num).slice(-2);
3 | }
4 |
5 | export function formatDate(value) {
6 | if(!value) return '';
7 | var d = new Date(value);
8 | return d instanceof Date && isFinite(d) ? `${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}` : '';
9 | }
10 |
11 | export function formatTime(value) {
12 | if(!value) return '';
13 | var d = new Date(value);
14 | return d instanceof Date && isFinite(d) ? `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}` : '';
15 | }
16 |
--------------------------------------------------------------------------------
/stub/semaphores.json:
--------------------------------------------------------------------------------
1 | {
2 | "SEMAPHORES": [
3 | {
4 | "ID": "entrance",
5 | "TITLE": "Entrance",
6 | "STATES": [
7 | {
8 | "STATE": "SHORT",
9 | "TIMESTAMP": "2015-08-12T10:31:00",
10 | "ACTION": "N/A",
11 | "CURRENT": "Y",
12 | "REASON": "Stand-in arrived, all booths staffed."
13 | },
14 | {
15 | "STATE": "MEDIUM",
16 | "TIMESTAMP": "2015-08-12T10:03:00",
17 | "ACTION": "Call stand-in",
18 | "CURRENT": "N",
19 | "REASON": "Employee called in sick"
20 | },
21 | {
22 | "STATE": "SHORT",
23 | "TIMESTAMP": "2015-08-12T10:00:00",
24 | "ACTION": "N/A",
25 | "CURRENT": "N",
26 | "REASON": "Initial",
27 | "REASON_BY": "System"
28 | }
29 | ]
30 | },
31 | {
32 | "ID": "icecream",
33 | "TITLE": "Ice Cream",
34 | "STATES": [
35 | {
36 | "STATE": "MEDIUM",
37 | "TIMESTAMP": "2015-08-12T11:10:00",
38 | "ACTION": "Get new ice cream",
39 | "CURRENT": "Y",
40 | "REASON": "Out of ice cream"
41 | },
42 | {
43 | "STATE": "SHORT",
44 | "TIMESTAMP": "2015-08-12T10:00:00",
45 | "ACTION": "N/A",
46 | "CURRENT": "N",
47 | "REASON": "Initial",
48 | "REASON_BY": "System"
49 | }
50 | ]
51 | },
52 | {
53 | "ID": "rollercoaster",
54 | "TITLE": "Roller Coaster",
55 | "STATES": [
56 | {
57 | "STATE": "SHORT",
58 | "TIMESTAMP": "2015-08-12T10:00:00",
59 | "ACTION": "N/A",
60 | "CURRENT": "Y",
61 | "REASON": "Initial",
62 | "REASON_BY": "System"
63 | }
64 | ]
65 | },
66 | {
67 | "ID": "waterride",
68 | "TITLE": "Water Ride",
69 | "STATES": [
70 | {
71 | "STATE": "SHORT",
72 | "TIMESTAMP": "2015-08-12T10:00:00",
73 | "ACTION": "N/A",
74 | "CURRENT": "Y",
75 | "REASON": "Initial",
76 | "REASON_BY": "System"
77 | }
78 | ]
79 | },
80 | {
81 | "ID": "burgers",
82 | "TITLE": "Burger Bar",
83 | "STATES": [
84 | {
85 | "STATE": "SHORT",
86 | "TIMESTAMP": "2015-08-12T10:00:00",
87 | "ACTION": "N/A",
88 | "CURRENT": "Y",
89 | "REASON": "Initial",
90 | "REASON_BY": "System"
91 | }
92 | ]
93 | },
94 | {
95 | "ID": "droptower",
96 | "TITLE": "Drop Tower",
97 | "STATES": [
98 | {
99 | "STATE": "SHORT",
100 | "TIMESTAMP": "2015-08-12T10:00:00",
101 | "ACTION": "N/A",
102 | "CURRENT": "Y",
103 | "REASON": "Initial",
104 | "REASON_BY": "System"
105 | }
106 | ]
107 | },
108 | {
109 | "ID": "wipeout",
110 | "TITLE": "Wipeout",
111 | "STATES": [
112 | {
113 | "STATE": "LONG",
114 | "TIMESTAMP": "2015-08-12T11:08:00",
115 | "ACTION": "Install backup system",
116 | "CURRENT": "Y",
117 | "REASON": "System was wiped out!"
118 | },
119 | {
120 | "STATE": "SHORT",
121 | "TIMESTAMP": "2015-08-12T10:00:00",
122 | "ACTION": "N/A",
123 | "CURRENT": "N",
124 | "REASON": "Initial",
125 | "REASON_BY": "System"
126 | }
127 | ]
128 | },
129 | {
130 | "ID": "milkshakes",
131 | "TITLE": "Milkshakes",
132 | "STATES": [
133 | {
134 | "STATE": "SHORT",
135 | "TIMESTAMP": "2015-08-12T10:00:00",
136 | "ACTION": "N/A",
137 | "CURRENT": "Y",
138 | "REASON": "Initial",
139 | "REASON_BY": "System"
140 | }
141 | ]
142 | },
143 | {
144 | "ID": "carousel",
145 | "TITLE": "Carousel",
146 | "STATES": [
147 | {
148 | "STATE": "MEDIUM",
149 | "TIMESTAMP": "2015-08-12T12:16:00",
150 | "ACTION": "Find the horse!",
151 | "CURRENT": "Y",
152 | "REASON": "One of the horses ran way"
153 | },
154 | {
155 | "STATE": "SHORT",
156 | "TIMESTAMP": "2015-08-12T10:00:00",
157 | "ACTION": "N/A",
158 | "CURRENT": "N",
159 | "REASON": "Initial",
160 | "REASON_BY": "System"
161 | }
162 | ]
163 | },
164 | {
165 | "ID": "hurricane",
166 | "TITLE": "Hurricane",
167 | "STATES": [
168 | {
169 | "STATE": "MEDIUM",
170 | "TIMESTAMP": "2015-08-12T10:00:00",
171 | "ACTION": "Direct people to roller coaster",
172 | "CURRENT": "Y",
173 | "REASON": "This attraction is popular today"
174 | },
175 | {
176 | "STATE": "SHORT",
177 | "TIMESTAMP": "2015-08-12T10:00:00",
178 | "ACTION": "N/A",
179 | "CURRENT": "N",
180 | "REASON": "Initial",
181 | "REASON_BY": "System"
182 | }
183 | ]
184 | },
185 | {
186 | "ID": "bungee",
187 | "TITLE": "Bungee Jump",
188 | "STATES": [
189 | {
190 | "STATE": "MEDIUM",
191 | "TIMESTAMP": "2015-08-12T10:00:00",
192 | "ACTION": "Jump quicker",
193 | "CURRENT": "Y",
194 | "REASON": "Cord fixed"
195 | },
196 | {
197 | "STATE": "LONG",
198 | "TIMESTAMP": "2015-08-12T10:00:00",
199 | "ACTION": "Fix cord",
200 | "CURRENT": "N",
201 | "REASON": "Broken cord"
202 | },
203 | {
204 | "STATE": "SHORT",
205 | "TIMESTAMP": "2015-08-12T10:00:00",
206 | "ACTION": "N/A",
207 | "CURRENT": "N",
208 | "REASON": "Initial",
209 | "REASON_BY": "System"
210 | }
211 | ]
212 | },
213 | {
214 | "ID": "exit",
215 | "TITLE": "Exit",
216 | "STATES": [
217 | {
218 | "STATE": "SHORT",
219 | "TIMESTAMP": "2015-08-12T10:00:00",
220 | "ACTION": "N/A",
221 | "CURRENT": "Y",
222 | "REASON": "Initial",
223 | "REASON_BY": "System"
224 | }
225 | ]
226 | }
227 | ]
228 | }
229 |
--------------------------------------------------------------------------------
/stub/shifts.json:
--------------------------------------------------------------------------------
1 | {
2 | "SHIFTS": [
3 | {
4 | "ID": "1",
5 | "TYPE": "MORNING",
6 | "START": "2015-08-07T10:00:00",
7 | "END": "2015-08-07T13:00:00"
8 | },
9 | {
10 | "ID": "2",
11 | "TYPE": "AFTERNOON",
12 | "START": "2015-08-07T14:00:00",
13 | "END": "2015-08-07T17:00:00"
14 | },
15 | {
16 | "ID": "3",
17 | "TYPE": "MORNING",
18 | "START": "2015-08-08T10:00:00",
19 | "END": "2015-08-08T13:00:00"
20 | },
21 | {
22 | "ID": "4",
23 | "TYPE": "AFTERNOON",
24 | "START": "2015-08-08T14:00:00",
25 | "END": "2015-08-08T17:00:00"
26 | },
27 | {
28 | "ID": "5",
29 | "TYPE": "MORNING",
30 | "START": "2015-08-09T10:00:00",
31 | "END": "2015-08-09T13:00:00"
32 | },
33 | {
34 | "ID": "6",
35 | "TYPE": "AFTERNOON",
36 | "START": "2015-08-09T14:00:00",
37 | "END": "2015-08-09T17:00:00"
38 | },
39 | {
40 | "ID": "7",
41 | "TYPE": "MORNING",
42 | "START": "2015-08-10T10:00:00",
43 | "END": "2015-08-10T13:00:00"
44 | },
45 | {
46 | "ID": "8",
47 | "TYPE": "AFTERNOON",
48 | "START": "2015-08-10T14:00:00",
49 | "END": "2015-08-10T17:00:00"
50 | },
51 | {
52 | "ID": "9",
53 | "TYPE": "MORNING",
54 | "START": "2015-08-11T10:00:00",
55 | "END": "2015-08-11T13:00:00"
56 | },
57 | {
58 | "ID": "10",
59 | "TYPE": "AFTERNOON",
60 | "START": "2015-08-11T14:00:00",
61 | "END": "2015-08-11T17:00:00"
62 | },
63 | {
64 | "ID": "11",
65 | "TYPE": "MORNING",
66 | "START": "2015-08-12T10:00:00",
67 | "END": "2015-08-12T13:00:00"
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/stub/state.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | webpack = require('webpack');
3 |
4 | var envPlugin = new webpack.DefinePlugin({
5 | 'process.env': {NODE_ENV:'"production"'},
6 | __DEV__: !process.env.BUILD_RELEASE
7 | });
8 |
9 | var plugins = [envPlugin];
10 |
11 | if(process.env.BUILD_RELEASE) {
12 | plugins.push(new webpack.optimize.DedupePlugin());
13 | plugins.push(new webpack.optimize.UglifyJsPlugin([]));
14 | } else {
15 | plugins.push(new webpack.HotModuleReplacementPlugin());
16 | plugins.push(new webpack.NoErrorsPlugin());
17 | }
18 |
19 | module.exports = {
20 | entry: './src/index.js',
21 | output: {
22 | path: path.resolve(__dirname, 'build'),
23 | publicPath: '/',
24 | filename: 'bundle.js'
25 | },
26 | module: {
27 | loaders: [
28 | {
29 | test: /\.jsx?$/,
30 | exclude: /(node_modules)/,
31 | loaders: ['react-hot', 'babel']
32 | }
33 | ]
34 | },
35 | plugins: plugins
36 | };
37 |
--------------------------------------------------------------------------------