├── src ├── containers │ ├── App │ │ ├── App.scss │ │ └── App.js │ ├── Home │ │ ├── logo.png │ │ ├── Home.js │ │ └── Home.scss │ ├── index.js │ ├── NotFound │ │ └── NotFound.js │ └── DevTools │ │ └── DevTools.js ├── controls │ └── Component.js ├── redux │ ├── modules │ │ ├── reducer.js │ │ └── auth.js │ ├── Logic.js │ ├── middleware │ │ └── clientMiddleware.js │ └── create.js ├── routes.js ├── config.js ├── helpers │ ├── ApiClient.js │ └── Html.js ├── utils │ └── validation.js ├── client.js └── server.js ├── .eslintignore ├── static ├── logo.jpg ├── favicon.ico └── favicon.png ├── tests.webpack.js ├── .gitignore ├── .editorconfig ├── .travis.yml ├── circle.yml ├── server.babel.js ├── app.json ├── .babelrc ├── bin └── server.js ├── .eslintrc ├── webpack ├── webpack-dev-server.js ├── prod.config.js ├── webpack-isomorphic-tools.js └── dev.config.js ├── LICENSE ├── karma.conf.js ├── CONTRIBUTING.md ├── package.json └── README.md /src/containers/App/App.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | karma.conf.js 3 | tests.webpack.js 4 | -------------------------------------------------------------------------------- /src/controls/Component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Robert on 7/26/16. 3 | */ 4 | -------------------------------------------------------------------------------- /static/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlancer/react-redux-starter-bare/master/static/logo.jpg -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlancer/react-redux-starter-bare/master/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlancer/react-redux-starter-bare/master/static/favicon.png -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./src', true, /-test\.js$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-assets.json 6 | webpack-stats.json 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /src/containers/Home/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlancer/react-redux-starter-bare/master/src/containers/Home/logo.png -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export App from './App/App'; 2 | export Home from './Home/Home'; 3 | export NotFound from './NotFound/NotFound'; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Doh! 404!

7 |

These are not the droids you are looking for!

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.12" 5 | - "4.0" 6 | - "4" 7 | - "5" 8 | - "stable" 9 | 10 | sudo: false 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | 16 | script: 17 | - npm run lint 18 | - npm test 19 | - npm run test-node 20 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.0 4 | environment: 5 | CONTINUOUS_INTEGRATION: true 6 | 7 | dependencies: 8 | cache_directories: 9 | - node_modules 10 | override: 11 | - npm prune && npm install 12 | 13 | test: 14 | override: 15 | - npm run lint 16 | - npm test 17 | - npm run test-node 18 | -------------------------------------------------------------------------------- /src/redux/modules/reducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import {routerReducer} from 'react-router-redux'; 3 | import {reducer as reduxAsyncConnect} from 'redux-connect'; 4 | 5 | import auth from './auth'; 6 | 7 | export default combineReducers({ 8 | routing: routerReducer, 9 | reduxAsyncConnect, 10 | auth 11 | }); 12 | -------------------------------------------------------------------------------- /server.babel.js: -------------------------------------------------------------------------------- 1 | // enable runtime transpilation to use ES6/7 in node 2 | 3 | var fs = require('fs'); 4 | 5 | var babelrc = fs.readFileSync('./.babelrc'); 6 | var config; 7 | 8 | try { 9 | config = JSON.parse(babelrc); 10 | } catch (err) { 11 | console.error('==> ERROR: Error parsing your .babelrc.'); 12 | console.error(err); 13 | } 14 | 15 | require('babel-register')(config); 16 | -------------------------------------------------------------------------------- /src/containers/DevTools/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/containers/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | 5 | export default class Home extends Component { 6 | render() { 7 | 8 | return ( 9 |
10 | 11 | 12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-universal-hot-example", 3 | "description": "Example of an isomorphic (universal) webapp using react redux and hot reloading", 4 | "repository": "https://github.com/erikras/react-redux-universal-hot-example", 5 | "logo": "http://node-js-sample.herokuapp.com/node.svg", 6 | "keywords": [ 7 | "react", 8 | "isomorphic", 9 | "universal", 10 | "webpack", 11 | "express", 12 | "hot reloading", 13 | "react-hot-reloader", 14 | "redux", 15 | "starter", 16 | "boilerplate", 17 | "babel" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {IndexRoute, Route} from 'react-router'; 3 | import {isLoaded as isAuthLoaded, load as loadAuth} from 'redux/modules/auth'; 4 | import { 5 | App, 6 | Chat, 7 | Home, 8 | Widgets, 9 | About, 10 | Login, 11 | LoginSuccess, 12 | Survey, 13 | NotFound, 14 | } from 'containers'; 15 | 16 | export default (store) => { 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | 4 | "plugins": [ 5 | "transform-runtime", 6 | "add-module-exports", 7 | "transform-decorators-legacy", 8 | "transform-react-display-name" 9 | ], 10 | 11 | "env": { 12 | "development": { 13 | "plugins": [ 14 | "typecheck", 15 | ["react-transform", { 16 | "transforms": [{ 17 | "transform": "react-transform-catch-errors", 18 | "imports": ["react", "redbox-react"] 19 | } 20 | ] 21 | }] 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import config from '../../config'; 4 | 5 | export default class App extends Component { 6 | static propTypes = { 7 | children: PropTypes.object.isRequired 8 | }; 9 | 10 | static contextTypes = { 11 | store: PropTypes.object.isRequired, 12 | logic: PropTypes.object.isRequired, 13 | }; 14 | 15 | render() { 16 | const styles = require('./App.scss'); 17 | 18 | return ( 19 |
20 | 21 | {this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/redux/Logic.js: -------------------------------------------------------------------------------- 1 | import {push} from 'react-router-redux'; 2 | 3 | export default class Logic { 4 | constructor({store}={}) { 5 | this._store = store; 6 | } 7 | 8 | routerPush(path) { 9 | this.dispatch(push(path)); 10 | } 11 | 12 | get location() { 13 | return this.state.routing.locationBeforeTransitions; 14 | } 15 | 16 | get path() { 17 | return this.location.pathname; 18 | } 19 | 20 | get loggedInUser() { 21 | return this.state.auth.user; 22 | } 23 | 24 | get isLoggedIn() { 25 | return !!this.state.auth.user; 26 | } 27 | 28 | get state() { 29 | return this.store.getState(); 30 | } 31 | 32 | get store() { 33 | return this._store; 34 | } 35 | 36 | dispatch(obj) { 37 | this.store.dispatch(obj); 38 | } 39 | } -------------------------------------------------------------------------------- /src/redux/middleware/clientMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function clientMiddleware(client) { 2 | return ({dispatch, getState}) => { 3 | return next => action => { 4 | if (typeof action === 'function') { 5 | return action(dispatch, getState); 6 | } 7 | 8 | const { promise, types, ...rest } = action; // eslint-disable-line no-redeclare 9 | if (!promise) { 10 | return next(action); 11 | } 12 | 13 | const [REQUEST, SUCCESS, FAILURE] = types; 14 | next({...rest, type: REQUEST}); 15 | 16 | const actionPromise = promise(client); 17 | actionPromise.then( 18 | (result) => next({...rest, result, type: SUCCESS}), 19 | (error) => next({...rest, error, type: FAILURE}) 20 | ).catch((error)=> { 21 | console.error('MIDDLEWARE ERROR:', error); 22 | next({...rest, error, type: FAILURE}); 23 | }); 24 | 25 | return actionPromise; 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../server.babel'); // babel registration (runtime transpilation for node) 3 | var path = require('path'); 4 | var rootDir = path.resolve(__dirname, '..'); 5 | /** 6 | * Define isomorphic constants. 7 | */ 8 | global.__CLIENT__ = false; 9 | global.__SERVER__ = true; 10 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 11 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; 12 | 13 | if (__DEVELOPMENT__) { 14 | if (!require('piping')({ 15 | hook: true, 16 | ignore: /(\/\.|~$|\.json|\.scss$)/i 17 | })) { 18 | return; 19 | } 20 | } 21 | 22 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 23 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 24 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools')) 25 | .development(__DEVELOPMENT__) 26 | .server(rootDir, function () { 27 | require('../src/server'); 28 | }); 29 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": "eslint-config-airbnb", 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "react/no-multi-comp": 0, 9 | "import/default": 0, 10 | "import/no-duplicates": 0, 11 | "import/named": 0, 12 | "import/namespace": 0, 13 | "import/no-unresolved": 0, 14 | "import/no-named-as-default": 2, 15 | "comma-dangle": 0, // not sure why airbnb turned this on. gross! 16 | "indent": [2, 2, {"SwitchCase": 1}], 17 | "no-console": 0, 18 | "no-alert": 0 19 | }, 20 | "plugins": [ 21 | "react", "import" 22 | ], 23 | "settings": { 24 | "import/parser": "babel-eslint", 25 | "import/resolve": { 26 | "moduleDirectory": ["node_modules", "src"] 27 | } 28 | }, 29 | "globals": { 30 | "__DEVELOPMENT__": true, 31 | "__CLIENT__": true, 32 | "__SERVER__": true, 33 | "__DISABLE_SSR__": true, 34 | "__DEVTOOLS__": true, 35 | "socket": true, 36 | "webpackIsomorphicTools": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webpack/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | var Express = require('express'); 2 | var webpack = require('webpack'); 3 | 4 | var config = require('../src/config'); 5 | var webpackConfig = require('./dev.config'); 6 | var compiler = webpack(webpackConfig); 7 | 8 | var host = config.host || 'localhost'; 9 | var port = (Number(config.port) + 1) || 3001; 10 | var serverOptions = { 11 | contentBase: 'http://' + host + ':' + port, 12 | quiet: true, 13 | noInfo: true, 14 | hot: true, 15 | inline: true, 16 | lazy: false, 17 | publicPath: webpackConfig.output.publicPath, 18 | headers: {'Access-Control-Allow-Origin': '*'}, 19 | stats: {colors: true} 20 | }; 21 | 22 | var app = new Express(); 23 | 24 | app.use(require('webpack-dev-middleware')(compiler, serverOptions)); 25 | app.use(require('webpack-hot-middleware')(compiler)); 26 | 27 | app.listen(port, function onAppListening(err) { 28 | if (err) { 29 | console.error(err); 30 | } else { 31 | console.info('==> 🚧 Webpack development server listening on port %s', port); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/containers/Home/Home.scss: -------------------------------------------------------------------------------- 1 | @import "../../theme/variables.scss"; 2 | 3 | .home { 4 | dd { 5 | margin-bottom: 15px; 6 | } 7 | } 8 | .masthead { 9 | background: #2d2d2d; 10 | padding: 40px 20px; 11 | color: white; 12 | text-align: center; 13 | .logo { 14 | $size: 200px; 15 | margin: auto; 16 | height: $size; 17 | width: $size; 18 | border-radius: $size / 2; 19 | border: 1px solid $cyan; 20 | box-shadow: inset 0 0 10px $cyan; 21 | vertical-align: middle; 22 | p { 23 | line-height: $size; 24 | margin: 0px; 25 | } 26 | img { 27 | width: 75%; 28 | margin: auto; 29 | } 30 | } 31 | h1 { 32 | color: $cyan; 33 | font-size: 4em; 34 | } 35 | h2 { 36 | color: #ddd; 37 | font-size: 2em; 38 | margin: 20px; 39 | } 40 | a { 41 | color: #ddd; 42 | } 43 | p { 44 | margin: 10px; 45 | } 46 | .humility { 47 | color: $humility; 48 | a { 49 | color: $humility; 50 | } 51 | } 52 | .github { 53 | font-size: 1.5em; 54 | } 55 | } 56 | 57 | .counterContainer { 58 | text-align: center; 59 | margin: 20px; 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Rasmussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/redux/create.js: -------------------------------------------------------------------------------- 1 | import {createStore as _createStore, applyMiddleware, compose} from 'redux'; 2 | import createMiddleware from './middleware/clientMiddleware'; 3 | import {routerMiddleware} from 'react-router-redux'; 4 | 5 | export default function createStore(history, client, data) { 6 | // Sync dispatched route actions to the history 7 | const reduxRouterMiddleware = routerMiddleware(history); 8 | 9 | const middleware = [createMiddleware(client), reduxRouterMiddleware]; 10 | 11 | let finalCreateStore; 12 | if (__DEVELOPMENT__ && __CLIENT__ && __DEVTOOLS__) { 13 | const {persistState} = require('redux-devtools'); 14 | const DevTools = require('../containers/DevTools/DevTools'); 15 | finalCreateStore = compose( 16 | applyMiddleware(...middleware), 17 | window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument(), 18 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) 19 | )(_createStore); 20 | } else { 21 | finalCreateStore = applyMiddleware(...middleware)(_createStore); 22 | } 23 | 24 | const reducer = require('./modules/reducer'); 25 | const store = finalCreateStore(reducer, data); 26 | 27 | 28 | if (__DEVELOPMENT__ && module.hot) { 29 | module.hot.accept('./modules/reducer', () => { 30 | store.replaceReducer(require('./modules/reducer')); 31 | }); 32 | } 33 | 34 | return store; 35 | } 36 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | 3 | const environment = { 4 | development: { 5 | isProduction: false 6 | }, 7 | production: { 8 | isProduction: true 9 | } 10 | }[process.env.NODE_ENV || 'development']; 11 | 12 | module.exports = Object.assign({ 13 | host: process.env.HOST || 'localhost', 14 | port: process.env.PORT, 15 | apiHost: process.env.APIHOST || 'localhost', 16 | apiPort: process.env.APIPORT, 17 | app: { 18 | title: 'React Redux Example', 19 | description: 'All the modern best practices in one example.', 20 | head: { 21 | titleTemplate: 'React Redux Example: %s', 22 | meta: [ 23 | {name: 'description', content: 'All the modern best practices in one example.'}, 24 | {charset: 'utf-8'}, 25 | {property: 'og:site_name', content: 'React Redux Example'}, 26 | {property: 'og:image', content: 'https://react-redux.herokuapp.com/logo.jpg'}, 27 | {property: 'og:locale', content: 'en_US'}, 28 | {property: 'og:title', content: 'React Redux Example'}, 29 | {property: 'og:description', content: 'All the modern best practices in one example.'}, 30 | {property: 'og:card', content: 'summary'}, 31 | {property: 'og:site', content: '@erikras'}, 32 | {property: 'og:creator', content: '@erikras'}, 33 | {property: 'og:image:width', content: '200'}, 34 | {property: 'og:image:height', content: '200'} 35 | ] 36 | } 37 | }, 38 | 39 | }, environment); 40 | -------------------------------------------------------------------------------- /src/helpers/ApiClient.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | import config from '../config'; 3 | 4 | const methods = ['get', 'post', 'put', 'patch', 'del']; 5 | 6 | function formatUrl(path) { 7 | const adjustedPath = path[0] !== '/' ? '/' + path : path; 8 | if (__SERVER__) { 9 | // Prepend host and port of the API server to the path. 10 | return 'http://' + config.apiHost + ':' + config.apiPort + adjustedPath; 11 | } 12 | // Prepend `/api` to relative URL, to proxy to API server. 13 | return '/api' + adjustedPath; 14 | } 15 | 16 | export default class ApiClient { 17 | constructor(req) { 18 | methods.forEach((method) => 19 | this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => { 20 | const request = superagent[method](formatUrl(path)); 21 | 22 | if (params) { 23 | request.query(params); 24 | } 25 | 26 | if (__SERVER__ && req.get('cookie')) { 27 | request.set('cookie', req.get('cookie')); 28 | } 29 | 30 | if (data) { 31 | request.send(data); 32 | } 33 | 34 | request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body)); 35 | })); 36 | } 37 | /* 38 | * There's a V8 bug where, when using Babel, exporting classes with only 39 | * constructors sometimes fails. Until it's patched, this is a solution to 40 | * "ApiClient is not defined" from issue #14. 41 | * https://github.com/erikras/react-redux-universal-hot-example/issues/14 42 | * 43 | * Relevant Babel bug (but they claim it's V8): https://phabricator.babeljs.io/T2455 44 | * 45 | * Remove it at your own risk. 46 | */ 47 | empty() {} 48 | } 49 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | browsers: ['PhantomJS'], 7 | 8 | singleRun: !!process.env.CI, 9 | 10 | frameworks: [ 'mocha' ], 11 | 12 | files: [ 13 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 14 | 'tests.webpack.js' 15 | ], 16 | 17 | preprocessors: { 18 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 19 | }, 20 | 21 | reporters: [ 'mocha' ], 22 | 23 | plugins: [ 24 | require("karma-webpack"), 25 | require("karma-mocha"), 26 | require("karma-mocha-reporter"), 27 | require("karma-phantomjs-launcher"), 28 | require("karma-sourcemap-loader") 29 | ], 30 | 31 | webpack: { 32 | devtool: 'inline-source-map', 33 | module: { 34 | loaders: [ 35 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} }, 36 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel']}, 37 | { test: /\.json$/, loader: 'json-loader' }, 38 | { test: /\.less$/, loader: 'style!css!less' }, 39 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' } 40 | ] 41 | }, 42 | resolve: { 43 | modulesDirectories: [ 44 | 'src', 45 | 'node_modules' 46 | ], 47 | extensions: ['', '.json', '.js'] 48 | }, 49 | plugins: [ 50 | new webpack.IgnorePlugin(/\.json$/), 51 | new webpack.NoErrorsPlugin(), 52 | new webpack.DefinePlugin({ 53 | __CLIENT__: true, 54 | __SERVER__: false, 55 | __DEVELOPMENT__: true, 56 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE 57 | }) 58 | ] 59 | }, 60 | 61 | webpackServer: { 62 | noInfo: true 63 | } 64 | 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/validation.js: -------------------------------------------------------------------------------- 1 | const isEmpty = value => value === undefined || value === null || value === ''; 2 | const join = (rules) => (value, data) => rules.map(rule => rule(value, data)).filter(error => !!error)[0 /* first error */ ]; 3 | 4 | export function email(value) { 5 | // Let's not start a debate on email regex. This is just for an example app! 6 | if (!isEmpty(value) && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { 7 | return 'Invalid email address'; 8 | } 9 | } 10 | 11 | export function required(value) { 12 | if (isEmpty(value)) { 13 | return 'Required'; 14 | } 15 | } 16 | 17 | export function minLength(min) { 18 | return value => { 19 | if (!isEmpty(value) && value.length < min) { 20 | return `Must be at least ${min} characters`; 21 | } 22 | }; 23 | } 24 | 25 | export function maxLength(max) { 26 | return value => { 27 | if (!isEmpty(value) && value.length > max) { 28 | return `Must be no more than ${max} characters`; 29 | } 30 | }; 31 | } 32 | 33 | export function integer(value) { 34 | if (!Number.isInteger(Number(value))) { 35 | return 'Must be an integer'; 36 | } 37 | } 38 | 39 | export function oneOf(enumeration) { 40 | return value => { 41 | if (!~enumeration.indexOf(value)) { 42 | return `Must be one of: ${enumeration.join(', ')}`; 43 | } 44 | }; 45 | } 46 | 47 | export function match(field) { 48 | return (value, data) => { 49 | if (data) { 50 | if (value !== data[field]) { 51 | return 'Do not match'; 52 | } 53 | } 54 | }; 55 | } 56 | 57 | export function createValidator(rules) { 58 | return (data = {}) => { 59 | const errors = {}; 60 | Object.keys(rules).forEach((key) => { 61 | const rule = join([].concat(rules[key])); // concat enables both functions and arrays of functions 62 | const error = rule(data[key], data); 63 | if (error) { 64 | errors[key] = error; 65 | } 66 | }); 67 | return errors; 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #459 refactor(utils): create url mapper utility function 29 | #463 chore(webpack): update to isomorphic tools v2 30 | #494 fix(babel): correct dependencies and polyfills 31 | #510 feat(app): add react-bootstrap responsive navbar 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `helpers`, `api` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import createStore from './redux/create'; 5 | import ApiClient from './helpers/ApiClient'; 6 | import {Provider} from 'react-redux'; 7 | import {Router, browserHistory} from 'react-router'; 8 | import {syncHistoryWithStore} from 'react-router-redux'; 9 | import {ReduxAsyncConnect} from 'redux-connect'; 10 | import useScroll from 'scroll-behavior/lib/useStandardScroll'; 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 12 | import Logic from './redux/Logic'; 13 | import injectTapEventPlugin from 'react-tap-event-plugin'; 14 | 15 | import getRoutes from './routes'; 16 | 17 | const client = new ApiClient(); 18 | const _browserHistory = useScroll(() => browserHistory)(); 19 | const dest = document.getElementById('content'); 20 | const store = createStore(_browserHistory, client, window.__data); 21 | const history = syncHistoryWithStore(_browserHistory, store); 22 | 23 | const logic = new Logic({store}); 24 | injectTapEventPlugin(); 25 | 26 | const MyProvider = Provider; 27 | MyProvider.prototype.getChildContext = function getChildContext() { 28 | return {store, logic}; 29 | }; 30 | 31 | Object.assign(MyProvider.childContextTypes, {logic: React.PropTypes.object}); 32 | 33 | const component = ( 34 | 35 | !item.deferred} /> 36 | } history={history}> 37 | {getRoutes(store)} 38 | 39 | ); 40 | 41 | ReactDOM.render( 42 | 43 | 44 | {component} 45 | 46 | , 47 | dest 48 | ); 49 | 50 | if (process.env.NODE_ENV !== 'production') { 51 | window.React = React; // enable debugger 52 | 53 | if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) { 54 | console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.'); 55 | } 56 | } 57 | 58 | if (__DEVTOOLS__ && !window.devToolsExtension) { 59 | const DevTools = require('./containers/DevTools/DevTools'); 60 | ReactDOM.render( 61 | 62 |
63 | {component} 64 | 65 |
66 |
, 67 | dest 68 | ); 69 | } -------------------------------------------------------------------------------- /src/helpers/Html.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import ReactDOM from 'react-dom/server'; 3 | import serialize from 'serialize-javascript'; 4 | import Helmet from 'react-helmet'; 5 | 6 | /** 7 | * Wrapper component containing HTML metadata and boilerplate tags. 8 | * Used in server-side code only to wrap the string output of the 9 | * rendered route component. 10 | * 11 | * The only thing this component doesn't (and can't) include is the 12 | * HTML doctype declaration, which is added to the rendered output 13 | * by the server.js file. 14 | */ 15 | export default class Html extends Component { 16 | static propTypes = { 17 | assets: PropTypes.object, 18 | component: PropTypes.node, 19 | store: PropTypes.object 20 | }; 21 | 22 | render() { 23 | const {assets, component, store} = this.props; 24 | const content = component ? ReactDOM.renderToString(component) : ''; 25 | const head = Helmet.rewind(); 26 | 27 | return ( 28 | 29 | 30 | {head.base.toComponent()} 31 | {head.title.toComponent()} 32 | {head.meta.toComponent()} 33 | {head.link.toComponent()} 34 | {head.script.toComponent()} 35 | 36 | 37 | 38 | {/* styles (will be present only in production with webpack extract text plugin) */} 39 | {Object.keys(assets.styles).map((style, key) => 40 | 42 | )} 43 | 44 | {/* (will be present only in development mode) */} 45 | {/* outputs a