├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── app ├── app.js ├── components │ ├── Layout.jsx │ ├── Navbar.jsx │ ├── Profile.jsx │ ├── Readme.jsx │ └── Users.jsx ├── decorators │ └── connectI18n.js ├── i18n │ ├── en.json │ └── fr.json ├── images │ └── favicon.ico ├── index.js ├── redux │ ├── actions │ │ ├── I18nActions.js │ │ ├── ReadmeActions.js │ │ └── UserActions.js │ ├── clientMiddleware.js │ ├── constants │ │ └── ActionTypes.js │ ├── create.js │ ├── reducers │ │ ├── i18n.js │ │ ├── index.js │ │ ├── readme.js │ │ └── users.js │ └── utils.js ├── routes.jsx ├── styles │ └── app.css └── utils │ ├── dev-tools.js │ ├── intl-loader.js │ └── localized-routes.js ├── logs └── .gitkeep ├── package.json ├── processes.json ├── server ├── api │ ├── data.json │ └── routes.js ├── express.js ├── index.js └── views │ └── index.ejs ├── shared ├── api-client.js ├── redux-resolver.js └── universal-render.jsx └── webpack ├── base.config.js ├── dev-server.js ├── dev.config.js ├── prod.config.js └── utils ├── start-express.js └── write-stats.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "env": { 4 | "browser": { 5 | "plugins": ["react-transform"], 6 | "extra": { 7 | "react-transform": { 8 | "transforms": [{ 9 | "transform": "react-transform-hmr", 10 | "imports": ["react"], 11 | "locals": ["module"] 12 | }, { 13 | "transform": "react-transform-catch-errors", 14 | "imports": ["react", "redbox-react"] 15 | }] 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | webpack/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint-config-airbnb", 4 | "plugins": ["react"], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true 9 | }, 10 | "rules": { 11 | "react/display-name": [2, { "acceptTranspilerName": true }], 12 | "react/jsx-curly-spacing": [2, "always"], 13 | "react/jsx-no-duplicate-props": 2, 14 | "react/jsx-no-undef": 2, 15 | "react/jsx-uses-react": 2, 16 | "react/jsx-uses-vars": 2, 17 | "react/no-did-mount-set-state": 2, 18 | "react/no-did-update-set-state": 2, 19 | "react/no-multi-comp": 2, 20 | "react/no-unknown-property": 2, 21 | "react/prop-types": 2, 22 | "react/react-in-jsx-scope": 2, 23 | "react/require-extension": 2, 24 | "react/self-closing-comp": 2, 25 | "react/wrap-multilines": 2, 26 | "react/sort-comp": 0, 27 | 28 | "react/jsx-closing-bracket-location": [2, { "selfClosing" : "after-props", "nonEmpty": "after-props" }], 29 | "react/jsx-indent-props": [2, 2], 30 | "react/prefer-es6-class": 2, 31 | 32 | "jsx-quotes": [2, "prefer-single"], 33 | "quotes": [2, "single", "avoid-escape"], 34 | "comma-dangle": [2, "never"], 35 | "indent": [2, 2, { "SwitchCase": 1 }], 36 | "object-curly-spacing": [2, "always"], 37 | "no-undef": 2, 38 | "no-underscore-dangle": 0, 39 | "func-names": 0, 40 | "no-else-return": 0 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .DS_Store 4 | server/webpack-stats.json 5 | dist/ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Redux Boilerplate 2 | 3 | > Isomorphic [Universal](https://medium.com/@mjackson/universal-javascript-4761051b7ae9) app with [redux](https://github.com/gaearon/redux) as Flux library and [redux-devtools](https://github.com/gaearon/redux-devtools) hot-reload tools 4 | 5 | ### Libraries 6 | 7 | * [expressjs](http://expressjs.com/) 8 | * [reactjs ^0.14](https://facebook.github.io/react/) 9 | * [react-router 1.0.0-rc3](http://rackt.github.io/react-router/tags/v1.0.0-beta3.html) 10 | * [redux ^3.0.0](https://github.com/gaearon/redux) 11 | * [redux-devtools ^3.0.0-beta-3](https://github.com/gaearon/redux-devtools) 12 | * [react-redux ^4.0.0](https://github.com/rackt/react-redux) 13 | * [postcss](https://github.com/postcss/postcss) 14 | * [precss](https://github.com/jonathantneal/precss) 15 | * [webpack](http://webpack.github.io) 16 | * [babel](http://babeljs.io) 17 | 18 | ## Documentation 19 | 20 | ### Async data-fetching 21 | 22 | [shared/redux-resolver.js](https://github.com/savemysmartphone/universal-redux-boilerplate/blob/master/shared/redux-resolver.js) is the magic thing about the boilerplate. It's our tool for resolving promises (data-fetching) before server side render. 23 | 24 | The resolver is available on the `store` instance through components context, use it to wrap your async actions in `componentWillMount` for data to be fetched before server side render: 25 | 26 | ```javascript 27 | import { bindActionCreators } from 'redux'; 28 | import * as Actions from 'redux/actions/Actions'; 29 | [...] 30 | static propTypes = { 31 | dispatch: PropTypes.func.isRequired 32 | } 33 | 34 | static contextTypes = { 35 | store: PropTypes.object.isRequired 36 | } 37 | 38 | componentWillMount() { 39 | const { dispatch } = this.props; 40 | const { resolver } = this.context.store; 41 | this.actions = bindActionCreators(Actions, dispatch); 42 | 43 | return resolver.resolve(this.actions.load, {id: 10}); 44 | } 45 | ``` 46 | 47 | The action `this.actions.load` will be resolved instantly on browser. On the other hand, on server side a first render `React.renderToString` is called to collect promises, resolve them and re-render with the correct data. 48 | 49 | ### How to / Installation 50 | 51 | * `$ git clone -o upstream https://github.com/savemysmartphone/universal-redux-boilerplate.git` 52 | * `$ cd universal-redux-boilerplate && npm install` 53 | * `$ npm run dev` 54 | 55 | (Don't forget to add your remote origin: `$ git remote add origin git@github.com:xxx/xxx.git`) 56 | 57 | ### Update the boilerplate 58 | 59 | You can fetch the upstream branch and merge it into your master: 60 | 61 | * `$ git checkout master` 62 | * `$ git fetch upstream` 63 | * `$ git merge upstream/master` 64 | * `$ npm install` 65 | 66 | ### Run in production 67 | 68 | * `$ npm run build` 69 | * `$ npm run prod` 70 | 71 | ### Learn more 72 | 73 | * [Official ReactJS website](http://facebook.github.io/react/) 74 | * [Official ReactJS wiki](https://github.com/facebook/react/wiki) 75 | * [Official Flux website](http://facebook.github.io/flux/) 76 | * [ReactJS Conf 2015 links](https://gist.github.com/yannickcr/148110d3ca658ad96c2b) 77 | * [Learn ES6](https://babeljs.io/docs/learn-es6/) 78 | * [ES6 Features](https://github.com/lukehoban/es6features#readme) 79 | 80 | ### Related projects 81 | 82 | * [gaeron/redux-devtools/examples](https://github.com/gaearon/redux-devtools/blob/master/examples%2Ftodomvc%2FREADME.md) 83 | * [iam4x/isomorphic-flux-boilerplate](https://github.com/iam4x/isomorphic-flux-boilerplate) 84 | * [erikas/react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) 85 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | import ReactDOM from 'react-dom'; 4 | import createBrowserHistory from 'history/lib/createBrowserHistory'; 5 | 6 | import createStore from './redux/create'; 7 | import ApiClient from '../shared/api-client'; 8 | import universalRender from '../shared/universal-render'; 9 | 10 | const { NODE_ENV, BROWSER } = process.env; 11 | 12 | if (NODE_ENV !== 'production') debug.enable('dev'); 13 | if (BROWSER) require('styles/app.css'); 14 | 15 | (async function() { 16 | try { 17 | const store = createStore(new ApiClient(), window.__state); 18 | const history = createBrowserHistory(); 19 | const container = window.document.getElementById('content'); 20 | const element = await universalRender({ history, store }); 21 | 22 | // render application in browser 23 | ReactDOM.render(element, container); 24 | 25 | // clean state of `redux-resolver` 26 | store.resolver.firstRender = false; 27 | store.resolver.pendingActions = []; 28 | } catch (error) { 29 | debug('dev')('Error with first render'); 30 | throw error; 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /app/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Navbar from 'components/Navbar'; 3 | 4 | class Layout extends Component { 5 | 6 | static propTypes = { 7 | children: PropTypes.element.isRequired 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 | 14 |
15 | { this.props.children } 16 |
17 |
18 | ); 19 | } 20 | 21 | } 22 | 23 | export default Layout; 24 | -------------------------------------------------------------------------------- /app/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import cx from 'classnames'; 5 | 6 | import * as I18nActions from 'redux/actions/I18nActions'; 7 | import connectI18n from 'decorators/connectI18n'; 8 | 9 | @connectI18n() 10 | class Navbar extends Component { 11 | 12 | static propTypes = { 13 | dispatch: PropTypes.func.isRequired, 14 | locale: PropTypes.string, 15 | messages: PropTypes.object 16 | } 17 | 18 | actions = bindActionCreators(I18nActions, this.props.dispatch) 19 | 20 | render() { 21 | return ( 22 | 53 | ); 54 | } 55 | 56 | } 57 | 58 | export default Navbar; 59 | -------------------------------------------------------------------------------- /app/components/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | import * as UserActions from 'redux/actions/UserActions'; 6 | 7 | @connect(({ users }) => ({ users })) 8 | class Profile extends Component { 9 | 10 | static propTypes = { 11 | dispatch: PropTypes.func.isRequired, 12 | users: PropTypes.object.isRequired, 13 | params: PropTypes.object.isRequired 14 | } 15 | 16 | static contextTypes = { store: PropTypes.object.isRequired } 17 | 18 | componentWillMount() { 19 | const { store: { resolver } } = this.context; 20 | const { dispatch, params: { seed } } = this.props; 21 | this.actions = bindActionCreators(UserActions, dispatch); 22 | 23 | return resolver.resolve(this.actions.show, seed); 24 | } 25 | 26 | componentWillUnmount() { 27 | this.actions.clearError(); 28 | } 29 | 30 | render() { 31 | const { params, users: { error, collection } } = this.props; 32 | const user = collection.find(({ seed }) => seed === params.seed); 33 | 34 | if (error) { 35 | return ( 36 |
37 | { error } 38 |
39 | ); 40 | } else if (!user) { 41 | return ( 42 |
43 | user not found 44 |
45 | ); 46 | } else { 47 | const { name: { first, last }, picture: { medium } } = user; 48 | return ( 49 |
50 |

{ first } { last }

51 | 52 |
53 | ); 54 | } 55 | } 56 | 57 | } 58 | 59 | export default Profile; 60 | -------------------------------------------------------------------------------- /app/components/Readme.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | import * as ReadmeActions from 'redux/actions/ReadmeActions'; 6 | 7 | @connect(({ readme }) => ({ readme })) 8 | class Readme extends Component { 9 | 10 | static propTypes = { 11 | readme: PropTypes.object.isRequired, 12 | dispatch: PropTypes.func.isRequired 13 | } 14 | 15 | static contextTypes = { store: PropTypes.object.isRequired } 16 | 17 | componentWillMount() { 18 | const { dispatch } = this.props; 19 | const { resolver } = this.context.store; 20 | this.actions = bindActionCreators(ReadmeActions, dispatch); 21 | 22 | return resolver.resolve(this.actions.load); 23 | } 24 | 25 | render() { 26 | const { readme: { error, markdown } } = this.props; 27 | if (error) return
{ error }
; 28 | 29 | return ( 30 |
33 | ); 34 | } 35 | 36 | } 37 | 38 | export default Readme; 39 | -------------------------------------------------------------------------------- /app/components/Users.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router'; 5 | 6 | import * as UserActions from 'redux/actions/UserActions'; 7 | import connectI18n from 'decorators/connectI18n'; 8 | 9 | @connect(({ users }) => ({ users })) 10 | @connectI18n() 11 | class Users extends Component { 12 | 13 | static propTypes = { 14 | users: PropTypes.object.isRequired, 15 | dispatch: PropTypes.func.isRequired 16 | } 17 | 18 | static contextTypes = { store: PropTypes.object.isRequired } 19 | 20 | componentWillMount() { 21 | const { resolver } = this.context.store; 22 | const { dispatch } = this.props; 23 | this.actions = bindActionCreators(UserActions, dispatch); 24 | 25 | return resolver.resolve(this.actions.index); 26 | } 27 | 28 | componentWillUnmount = () => this.actions.clearError() 29 | 30 | render() { 31 | const { users: { error, collection } } = this.props; 32 | if (error) { 33 | return ( 34 |
35 | { error } 36 |
37 | ); 38 | } else { 39 | return ( 40 |
41 |

{ this.i18n('users') }

42 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | } 60 | 61 | export default Users; 62 | -------------------------------------------------------------------------------- /app/decorators/connectI18n.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { IntlMixin } from 'react-intl'; 4 | 5 | export default function connectI18n() { 6 | return function(DecoratedComponent) { 7 | class WrapperComponent extends DecoratedComponent { 8 | // prevent app to break when translation is missing 9 | // add message a la i18n Rails 10 | i18n = (key, values) => { 11 | try { 12 | const messages = IntlMixin.getIntlMessage.call(this, key); 13 | return IntlMixin 14 | .formatMessage.call({ ...this, ...IntlMixin }, messages, values); 15 | } catch (error) { 16 | return `translation missing ${this.props.locale}: ${key}`; 17 | } 18 | } 19 | } 20 | 21 | @connect(({ i18n }) => ({ ...i18n })) 22 | class I18nWrapper extends Component { 23 | 24 | static propTypes = { 25 | locales: PropTypes.array.isRequired, 26 | messages: PropTypes.object.isRequired, 27 | formats: PropTypes.object 28 | } 29 | 30 | static childContextTypes = { 31 | locales: PropTypes.array.isRequired, 32 | messages: PropTypes.object.isRequired, 33 | formats: PropTypes.object 34 | } 35 | 36 | getChildContext() { 37 | const { messages, formats, locales } = this.props; 38 | return { messages, formats, locales }; 39 | } 40 | 41 | render() { 42 | return (); 43 | } 44 | 45 | } 46 | 47 | return I18nWrapper; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /app/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": { 3 | "users": "/users", 4 | "readme": "/readme", 5 | "profile": "/users/{seed}" 6 | }, 7 | "users": "Users", 8 | "readme": "Readme" 9 | } 10 | -------------------------------------------------------------------------------- /app/i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": { 3 | "users": "/utilisateurs", 4 | "readme": "/lisez-moi", 5 | "profile": "/utilisateurs/{seed}" 6 | }, 7 | "users": "Utilisateurs", 8 | "readme": "Documentation" 9 | } 10 | -------------------------------------------------------------------------------- /app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savemysmartphone/universal-redux-boilerplate/d6d20cbabcbab38b0e7784d28ecb5ff28bd9f7f3/app/images/favicon.ico -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | // require ES6/ES7 polyfill on browser 2 | require('babel/polyfill'); 3 | 4 | // Intl polyfill 5 | require('intl'); 6 | 7 | // start client application 8 | require('./app'); 9 | -------------------------------------------------------------------------------- /app/redux/actions/I18nActions.js: -------------------------------------------------------------------------------- 1 | import { LOCALE_INITIALIZE } from '../constants/ActionTypes'; 2 | import * as loaders from 'utils/intl-loader'; 3 | 4 | import { asyncFuncCreator } from '../utils'; 5 | 6 | export function change(locale = 'en') { 7 | return asyncFuncCreator({ 8 | constant: 'LOCALE_CHANGE', 9 | promise: loaders[locale], 10 | locale 11 | }); 12 | } 13 | 14 | export function initialize(locale, messages) { 15 | return { type: LOCALE_INITIALIZE, locale, messages }; 16 | } 17 | -------------------------------------------------------------------------------- /app/redux/actions/ReadmeActions.js: -------------------------------------------------------------------------------- 1 | import { asyncFuncCreator } from '../utils'; 2 | 3 | export function load() { 4 | return asyncFuncCreator({ 5 | constant: 'README_LOAD', 6 | promise: (client) => client.request({ url: '/readme' }) 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /app/redux/actions/UserActions.js: -------------------------------------------------------------------------------- 1 | import { USERS_CLEAR_ERROR } from '../constants/ActionTypes'; 2 | import { asyncFuncCreator } from '../utils'; 3 | 4 | export function index() { 5 | return asyncFuncCreator({ 6 | constant: 'USERS_INDEX', 7 | promise: (client) => client.request({ url: '/users' }) 8 | }); 9 | } 10 | 11 | export function show(seed) { 12 | return asyncFuncCreator({ 13 | constant: 'USERS_SHOW', 14 | promise: (client) => client.request({ url: '/users/' + seed }) 15 | }); 16 | } 17 | 18 | export function clearError() { 19 | return { type: USERS_CLEAR_ERROR }; 20 | } 21 | -------------------------------------------------------------------------------- /app/redux/clientMiddleware.js: -------------------------------------------------------------------------------- 1 | // // from https://github.com/erikras/react-redux-universal-hot-example/blob/master/src%2Fredux%2FclientMiddleware.js 2 | export default function clientMiddleware(client) { 3 | return () => { 4 | return (next) => (action) => { 5 | const { promise, types, ... rest } = action; 6 | if (!promise) return next(action); 7 | 8 | const [ REQUEST, SUCCESS, FAILURE ] = types; 9 | next({ ...rest, type: REQUEST }); 10 | return promise(client).then( 11 | (result) => next({ ...rest, result, type: SUCCESS }), 12 | (error) => next({ ...rest, error, type: FAILURE }) 13 | ); 14 | }; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /app/redux/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | import { generateConstants } from '../utils'; 2 | 3 | export default generateConstants([ 4 | 'USERS_SHOW', 5 | 'USERS_CLEAR_ERROR', 6 | 'USERS_INDEX(ASYNC)', 7 | 8 | 'README_LOAD(ASYNC)', 9 | 10 | 'LOCALE_INITIALIZE', 11 | 'LOCALE_CHANGE(ASYNC)' 12 | ]); 13 | -------------------------------------------------------------------------------- /app/redux/create.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 2 | import { persistState } from 'redux-devtools'; 3 | 4 | import DevTools from '../utils/dev-tools'; 5 | import createMiddleware from './clientMiddleware'; 6 | import * as reducers from './reducers'; 7 | 8 | const { NODE_ENV, BROWSER } = process.env; 9 | const reducer = combineReducers(reducers); 10 | 11 | export default function(client, data) { 12 | const middleware = createMiddleware(client); 13 | 14 | let finalCreateStore; 15 | if (process.env.BROWSER) { 16 | finalCreateStore = compose( 17 | applyMiddleware(middleware), 18 | DevTools.instrument(), 19 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) 20 | )(createStore); 21 | } else { 22 | finalCreateStore = applyMiddleware(middleware)(createStore); 23 | } 24 | 25 | const store = finalCreateStore(reducer, data); 26 | 27 | if (BROWSER && NODE_ENV === 'developement' && module.hot) { 28 | module.hot.accept('./reducers', () => 29 | store.replaceReducer(require('./reducers'))); 30 | } 31 | 32 | return store; 33 | } 34 | -------------------------------------------------------------------------------- /app/redux/reducers/i18n.js: -------------------------------------------------------------------------------- 1 | import at from '../constants/ActionTypes'; 2 | 3 | const initialState = { messages: {}, formats: {}, locales: [ 'en' ] }; 4 | 5 | export default function i18n(state = initialState, action) { 6 | const { type, result, locale, messages } = action; 7 | 8 | switch (type) { 9 | case at.LOCALE_CHANGE: 10 | return { ...state, loading: true }; 11 | 12 | case at.LOCALE_CHANGE_SUCCESS: 13 | return { ...state, messages: result, locales: [ locale ], loading: false }; 14 | 15 | case at.LOCALE_CHANGE_FAIL: 16 | const { error } = result; 17 | return { ...state, loading: false, error }; 18 | 19 | case at.LOCALE_INITIALIZE: 20 | return { ...state, messages, locales: [ locale ] }; 21 | 22 | default: 23 | return state; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as users } from './users'; 2 | export { default as readme } from './readme'; 3 | export { default as i18n } from './i18n'; 4 | -------------------------------------------------------------------------------- /app/redux/reducers/readme.js: -------------------------------------------------------------------------------- 1 | import * as t from '../constants/ActionTypes'; 2 | 3 | const initialState = { markdown: '' }; 4 | 5 | export default function readme(state = initialState, action) { 6 | switch (action.type) { 7 | case t.README_LOAD: 8 | return { ...state, loading: true }; 9 | 10 | case t.README_LOAD_SUCCESS: 11 | return { ...state, loading: false, markdown: action.result }; 12 | 13 | case t.README_LOAD_FAIL: 14 | return { ...state, loading: false, error: action.error }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/redux/reducers/users.js: -------------------------------------------------------------------------------- 1 | import at from '../constants/ActionTypes'; 2 | 3 | const initialState = { collection: [] }; 4 | 5 | export default function users(state = initialState, action) { 6 | switch (action.type) { 7 | case at.USERS_INDEX: 8 | return { ...state, loading: true }; 9 | 10 | case at.USERS_INDEX_SUCCESS: 11 | return { ...state, loading: false, collection: action.result }; 12 | 13 | case at.USERS_INDEX_FAIL: 14 | return { ...state, loading: false, error: action.error }; 15 | 16 | case at.USERS_SHOW: 17 | return { ...state, loading: true }; 18 | 19 | case at.USERS_SHOW_SUCCESS: 20 | // clone `state.collection` 21 | let collection = [ ...state.collection ]; 22 | 23 | // find fetched user into collection 24 | const { seed } = action.result; 25 | if (!collection.find(user => user.seed === seed)) { 26 | collection = [ action.result, ...state.collection ]; 27 | } 28 | 29 | // return modified state 30 | return { ...state, loading: false, collection }; 31 | 32 | case at.USERS_SHOW_FAIL: 33 | return { ...state, loading: false, error: action.error }; 34 | 35 | case at.USERS_CLEAR_ERROR: 36 | return { ...state, error: null }; 37 | 38 | default: 39 | return state; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/redux/utils.js: -------------------------------------------------------------------------------- 1 | // Utils for removing boilerplate from Redux 2 | import at from './constants/ActionTypes'; 3 | 4 | export function asyncFuncCreator({ constant, ...rest }) { 5 | return { 6 | types: [ 7 | at[constant], 8 | at[constant + '_SUCCESS'], 9 | at[constant + '_FAIL'] 10 | ], 11 | ...rest 12 | }; 13 | } 14 | 15 | export function generateConstants(constants) { 16 | return constants.reduce((result, constant) => { 17 | if (constant.indexOf('(ASYNC)')) { 18 | const clean = constant.replace('(ASYNC)', ''); 19 | result[clean] = clean; 20 | result[clean + '_SUCCESS'] = clean + '_SUCCESS'; 21 | result[clean + '_FAIL'] = clean + '_FAIL'; 22 | } else { 23 | result[constant] = constant; 24 | } 25 | return result; 26 | }, {}); 27 | } 28 | -------------------------------------------------------------------------------- /app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | import { generateRoute } from 'utils/localized-routes'; 5 | 6 | export default ( 7 | 8 | { generateRoute({ 9 | paths: [ '/', '/users', '/utilisateurs' ], 10 | component: require('components/Users') 11 | }) } 12 | { generateRoute({ 13 | paths: [ '/users/:seed', '/utilisateurs/:seed' ], 14 | component: require('components/Profile') 15 | }) } 16 | { generateRoute({ 17 | paths: [ '/readme', '/lisez-moi' ], 18 | component: require('components/Readme') 19 | }) } 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/dist/css/bootstrap'; 2 | 3 | body { 4 | padding-top: 100px; 5 | } 6 | 7 | /* on dev make redux-devtool always shown */ 8 | .navbar-fixed-top { 9 | z-index: 998; 10 | } 11 | 12 | .user-list ul, 13 | .user-list li { 14 | list-style-type: none; 15 | padding: 10px 0px; 16 | } 17 | 18 | .user-list li { 19 | padding: 10px 20px; 20 | } 21 | 22 | .user-list li img { 23 | margin-right: 10px; 24 | } 25 | -------------------------------------------------------------------------------- /app/utils/dev-tools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import DockMonitor from 'redux-devtools-dock-monitor'; 4 | import LogMonitor from 'redux-devtools-log-monitor'; 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /app/utils/intl-loader.js: -------------------------------------------------------------------------------- 1 | // We need to define `ReactIntl` on the global scope 2 | // in order to load specific locale data from `ReactIntl` 3 | // see: https://github.com/iam4x/isomorphic-flux-boilerplate/issues/64 4 | const { BROWSER } = process.env; 5 | if (BROWSER) window.ReactIntl = require('react-intl'); 6 | 7 | export function en() { 8 | return new Promise((resolve) => { 9 | if (BROWSER) { 10 | require.ensure([ 11 | 'intl', 12 | 'intl/locale-data/jsonp/en', 13 | 'i18n/en.json' 14 | ], (require) => { 15 | require('intl'); 16 | require('intl/locale-data/jsonp/en'); 17 | return resolve(require('i18n/en.json')); 18 | }); 19 | } else { 20 | return resolve(require('i18n/en.json')); 21 | } 22 | }); 23 | } 24 | 25 | export function fr() { 26 | return new Promise((resolve) => { 27 | if (BROWSER) { 28 | require.ensure([ 29 | 'intl', 30 | 'intl/locale-data/jsonp/fr', 31 | 'i18n/fr.json' 32 | ], (require) => { 33 | require('intl'); 34 | require('intl/locale-data/jsonp/fr'); 35 | return resolve(require('i18n/fr.json')); 36 | }); 37 | } else { 38 | return resolve(require('i18n/fr.json')); 39 | } 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /app/utils/localized-routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | export function generateRoute({ paths, component }) { 5 | /* eslint react/display-name:0 */ 6 | // see: https://github.com/yannickcr/eslint-plugin-react/issues/256 7 | return paths.map(function(path) { 8 | const props = { key: path, path, component }; 9 | if (component.onEnter) props.onEnter = component.onEnter; 10 | return ; 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savemysmartphone/universal-redux-boilerplate/d6d20cbabcbab38b0e7784d28ecb5ff28bd9f7f3/logs/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-redux-boilerplate", 3 | "version": "0.0.1", 4 | "description": "An universal (isomorphic) boilerplate for ReactJS using Redux as Flux library.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "BABEL_ENV=browser babel-node ./webpack/dev-server", 8 | "prod": "babel-node ./server/index", 9 | "build": "rm -rf dist/* && babel-node ./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "lint": "eslint --ext .js,.jsx app server shared" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/savemysmartphone/universal-redux-boilerplate.git" 16 | }, 17 | "homepage": "https://github.com/savemysmartphone/universal-redux-boilerplate", 18 | "bugs": "https://github.com/savemysmartphone/universal-redux-boilerplate/issues", 19 | "author": "iam4x", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "autoprefixer": "^6.0.3", 23 | "babel-core": "^5.8.22", 24 | "babel-eslint": "^4.0.10", 25 | "babel-loader": "^5.3.2", 26 | "babel-plugin-react-transform": "^1.0.2", 27 | "browser-sync": "^2.8.2", 28 | "css-loader": "^0.21.0", 29 | "dev-ip": "^1.0.1", 30 | "eslint": "^1.2.1", 31 | "eslint-config-airbnb": "^0.1.0", 32 | "eslint-loader": "^1.0.0", 33 | "eslint-plugin-react": "^3.2.3", 34 | "extract-text-webpack-plugin": "^0.8.2", 35 | "file-loader": "^0.8.4", 36 | "image-loader": "0.0.1", 37 | "json-loader": "^0.5.2", 38 | "node-libs-browser": "^0.5.2", 39 | "node-watch": "^0.3.4", 40 | "postcss-import": "^7.1.0", 41 | "postcss-loader": "^0.7.0", 42 | "postcss-url": "^5.0.2", 43 | "precss": "^1.3.0", 44 | "react-transform-catch-errors": "^1.0.0", 45 | "react-transform-hmr": "^1.0.1", 46 | "redbox-react": "^1.0.1", 47 | "redux-devtools": "^3.0.0-beta-3", 48 | "redux-devtools-dock-monitor": "^1.0.0-beta-3", 49 | "redux-devtools-log-monitor": "^1.0.0-beta-3", 50 | "style-loader": "^0.13.0", 51 | "webpack": "^1.12.0", 52 | "webpack-dev-middleware": "^1.2.0", 53 | "webpack-hot-middleware": "^2.0.0" 54 | }, 55 | "dependencies": { 56 | "axios": "^0.7.0", 57 | "babel": "^5.8.21", 58 | "body-parser": "^1.13.3", 59 | "bootstrap": "^3.3.5", 60 | "classnames": "^2.1.3", 61 | "compression": "^1.5.2", 62 | "debug": "^2.2.0", 63 | "ejs": "^2.3.3", 64 | "express": "^4.13.3", 65 | "fs-promise": "^0.3.1", 66 | "helmet": "^0.14.0", 67 | "history": "^1.12.5", 68 | "intl": "^1.0.0", 69 | "lodash": "^3.10.1", 70 | "marked": "^0.3.5", 71 | "morgan": "^1.6.1", 72 | "react": "^0.14.0", 73 | "react-dom": "^0.14.0", 74 | "react-intl": "^1.2.0", 75 | "react-redux": "^4.0.0", 76 | "react-router": "^1.0.0-rc3", 77 | "redux": "^3.0.2", 78 | "response-time": "^2.3.1", 79 | "serialize-javascript": "^1.0.0", 80 | "serve-favicon": "^2.3.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "universal-redux-boilerplate", 5 | "script": "./server/index.js", 6 | "instances": 4, 7 | "log_date_format": "YYYY-MM-DD HH:mm Z", 8 | "error_file": "./logs/app-err.log", 9 | "out_file": "./logs/app-out.log", 10 | "exec_mode": "cluster_mode", 11 | "max_memory_restart": "1750M", 12 | "watch": false, 13 | "env": { "DEBUG": "server" }, 14 | "node_args": "--max_old_space_size=1750" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /server/api/data.json: -------------------------------------------------------------------------------- 1 | {"users":[{"user":{"gender":"female","name":{"title":"ms","first":"clara","last":"coleman"},"location":{"street":"7855 valwood pkwy","city":"evansville","state":"delaware","zip":"38095"},"email":"clara.coleman83@example.com","username":"smallsnake436","password":"total","salt":"ROOujBwn","md5":"3719d92a9a409bb329538929cd1b3549","sha1":"81f58d15787d3e0a63685facfa139399f05f947c","sha256":"0687fe39adb0e43c28c8ffb70e84baa2ea2e1bae0afa349db31b4e861208ec8e","registered":"1238304997","dob":"56822726","phone":"(951)-385-6121","cell":"(657)-919-3511","SSN":"214-92-8644","picture":{"large":"http://api.randomuser.me/portraits/women/72.jpg","medium":"http://api.randomuser.me/portraits/med/women/72.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/72.jpg"},"version":"0.5","nationality":"US"},"seed":"7729a1ef4ba6ef68"},{"user":{"gender":"male","name":{"title":"mr","first":"jared","last":"silva"},"location":{"street":"4635 lone wolf trail","city":"columbus","state":"pennsylvania","zip":"87898"},"email":"jared.silva87@example.com","username":"redgoose810","password":"newcastl","salt":"aIKQH0OL","md5":"f0b78307c7483cf88e83e963b653b938","sha1":"d0f471050181a2639374083fb6cb5d2073cd7685","sha256":"c4d7e327c514b4e652e4199b3936d96e63498541dd435ba571d0c385f06a5fd5","registered":"1241177745","dob":"436110816","phone":"(500)-329-6851","cell":"(706)-536-2253","SSN":"371-32-4308","picture":{"large":"http://api.randomuser.me/portraits/men/76.jpg","medium":"http://api.randomuser.me/portraits/med/men/76.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/76.jpg"},"version":"0.5","nationality":"US"},"seed":"ca924b030680223c"},{"user":{"gender":"male","name":{"title":"mr","first":"john","last":"freeman"},"location":{"street":"6180 spring hill rd","city":"carrollton","state":"nevada","zip":"74600"},"email":"john.freeman24@example.com","username":"blackwolf691","password":"floppy","salt":"Y0jWM5E7","md5":"681313537623ab3fe9aaea2e1570095a","sha1":"0a95cf080607dc09ba76d079fc3f6a75265db53b","sha256":"640b1cccc46db3a6fee4e04838e4dedd1412d413f51d663e9dcf81a5892545f7","registered":"1229059280","dob":"303862072","phone":"(321)-632-7066","cell":"(546)-346-9012","SSN":"473-84-7955","picture":{"large":"http://api.randomuser.me/portraits/men/51.jpg","medium":"http://api.randomuser.me/portraits/med/men/51.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/51.jpg"},"version":"0.5","nationality":"US"},"seed":"e065bfd9ee67b78d"},{"user":{"gender":"male","name":{"title":"mr","first":"lewis","last":"ellis"},"location":{"street":"9388 e little york rd","city":"roseburg","state":"south dakota","zip":"49483"},"email":"lewis.ellis64@example.com","username":"silverkoala652","password":"mylife","salt":"wte7IXHT","md5":"1e9801177be2f6ab0d62c89174a50589","sha1":"b8f2e7b3cc23761a7966776f1a85350c9a47a72b","sha256":"b1108ab77755fc0cdc5fd2cf4604f1c17bbf1dd7060dfa3b4a3024794a6dbd97","registered":"1046419757","dob":"127638318","phone":"(422)-465-1890","cell":"(475)-417-1083","SSN":"483-59-9967","picture":{"large":"http://api.randomuser.me/portraits/men/54.jpg","medium":"http://api.randomuser.me/portraits/med/men/54.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/54.jpg"},"version":"0.5","nationality":"US"},"seed":"07b58bd6a498f3cf"},{"user":{"gender":"male","name":{"title":"mr","first":"daryl","last":"freeman"},"location":{"street":"3772 valley view ln","city":"salt lake city","state":"georgia","zip":"63675"},"email":"daryl.freeman28@example.com","username":"bigrabbit287","password":"glory","salt":"DOTNlUEK","md5":"dd0052de70e4c574778be1f3996e3282","sha1":"29d1e649fa8353769228714c241287bac8477773","sha256":"44ecae42bf0a0b27afcd7f819474b420b97ea62ff97d1365eb5f25c1d7e2f4db","registered":"1160972856","dob":"8945385","phone":"(201)-802-7645","cell":"(257)-429-8979","SSN":"336-93-8791","picture":{"large":"http://api.randomuser.me/portraits/men/32.jpg","medium":"http://api.randomuser.me/portraits/med/men/32.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/32.jpg"},"version":"0.5","nationality":"US"},"seed":"56084f8779613486"},{"user":{"gender":"female","name":{"title":"mrs","first":"isabella","last":"miles"},"location":{"street":"4232 robinson rd","city":"salt lake city","state":"west virginia","zip":"26409"},"email":"isabella.miles83@example.com","username":"silvercat60","password":"lumber","salt":"b2CyicaV","md5":"04cb99199928c42d0006ac2ba07a9acc","sha1":"b017c1c49c40bc67218640276d900617ee48e4db","sha256":"24857094077c6b0e4d16e7b82cb0927781c1510312e626a2f4047241862c8ee0","registered":"1222000564","dob":"437677588","phone":"(269)-620-4255","cell":"(490)-266-9416","SSN":"417-49-2706","picture":{"large":"http://api.randomuser.me/portraits/women/7.jpg","medium":"http://api.randomuser.me/portraits/med/women/7.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/7.jpg"},"version":"0.5","nationality":"US"},"seed":"f60429024169712c"},{"user":{"gender":"female","name":{"title":"ms","first":"lena","last":"pierce"},"location":{"street":"4820 ranchview dr","city":"lewiston","state":"missouri","zip":"30113"},"email":"lena.pierce40@example.com","username":"greenfrog851","password":"stoney","salt":"1uvCLAwD","md5":"dd3ec7d4a33795b07244b072b6539e80","sha1":"be67fee8b803918f52489dbd8b1b9de7cf0a5adf","sha256":"5ee8b6f8e4d0dab6a812abf2f8e7abb8fca34a41248cb0bb9b20c56a7e4c7fcb","registered":"1110169527","dob":"328808909","phone":"(511)-489-2831","cell":"(543)-221-4315","SSN":"729-88-7174","picture":{"large":"http://api.randomuser.me/portraits/women/2.jpg","medium":"http://api.randomuser.me/portraits/med/women/2.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/2.jpg"},"version":"0.5","nationality":"US"},"seed":"5c08995882c73097"},{"user":{"gender":"female","name":{"title":"miss","first":"annie","last":"perkins"},"location":{"street":"9390 w pecan st","city":"ennis","state":"south dakota","zip":"11462"},"email":"annie.perkins51@example.com","username":"heavywolf917","password":"sebastian","salt":"xtXxUwji","md5":"beee34abb457cc87c3f10f00e3c7ab90","sha1":"dc305dfa16b31ae4c5eaede23a24704de51ee97d","sha256":"7cd4f52329dea69357455c5dd1343c3c7dfa9a74426f12ee63e116116dfecd94","registered":"1126393198","dob":"384579771","phone":"(566)-602-3590","cell":"(741)-461-4021","SSN":"760-88-2509","picture":{"large":"http://api.randomuser.me/portraits/women/72.jpg","medium":"http://api.randomuser.me/portraits/med/women/72.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/72.jpg"},"version":"0.5","nationality":"US"},"seed":"5271b2f4e31b3951"},{"user":{"gender":"male","name":{"title":"mr","first":"todd","last":"davis"},"location":{"street":"8141 daisy dr","city":"bernalillo","state":"idaho","zip":"90912"},"email":"todd.davis91@example.com","username":"smallcat507","password":"illusion","salt":"1iQHPcFz","md5":"85a0f3825232ab237035242e6784cd4c","sha1":"dbeed90df67bdad5c35eca0f9973dda28f9c5e46","sha256":"19e93d85eb7e5c8d6e929384d941e895537eb8918dea5aa7c47a641fae600e5d","registered":"1366069875","dob":"190226277","phone":"(588)-972-6277","cell":"(451)-628-2064","SSN":"833-99-5558","picture":{"large":"http://api.randomuser.me/portraits/men/58.jpg","medium":"http://api.randomuser.me/portraits/med/men/58.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/58.jpg"},"version":"0.5","nationality":"US"},"seed":"e730687a9438a8f0"},{"user":{"gender":"female","name":{"title":"ms","first":"kylie","last":"rogers"},"location":{"street":"8064 depaul dr","city":"surrey","state":"connecticut","zip":"41249"},"email":"kylie.rogers47@example.com","username":"heavyfrog740","password":"highheel","salt":"6C5TX45S","md5":"590da689565cf3374b38670cf35d1c75","sha1":"0cf4286a896f234cf0dbcca9a5640bacf03e2bb2","sha256":"f8d4c695796fb336857f99be5368ecc3e094e862ff1bee5856b79b068d90c92d","registered":"1306353110","dob":"420932995","phone":"(648)-257-2776","cell":"(950)-918-3951","SSN":"804-61-8979","picture":{"large":"http://api.randomuser.me/portraits/women/31.jpg","medium":"http://api.randomuser.me/portraits/med/women/31.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/31.jpg"},"version":"0.5","nationality":"US"},"seed":"a70e64b8c7516cb7"},{"user":{"gender":"male","name":{"title":"mr","first":"andre","last":"gonzalez"},"location":{"street":"6022 valley view ln","city":"new york","state":"indiana","zip":"30321"},"email":"andre.gonzalez57@example.com","username":"bigbird168","password":"patience","salt":"GFNvSXyi","md5":"df79851a1314b35de2be11eb37d33791","sha1":"6510ac2ad32ec68a3b3c434810aefc9cebeed97e","sha256":"050719d92ea883eb04517ef971fc3069d2eb4f218ba5e0531550ac8acba403f5","registered":"1418999142","dob":"152945987","phone":"(855)-619-6424","cell":"(972)-621-7492","SSN":"649-16-2110","picture":{"large":"http://api.randomuser.me/portraits/men/20.jpg","medium":"http://api.randomuser.me/portraits/med/men/20.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/20.jpg"},"version":"0.5","nationality":"US"},"seed":"11fb37965e0c61c7"},{"user":{"gender":"male","name":{"title":"mr","first":"bernard","last":"hansen"},"location":{"street":"2503 college st","city":"san diego","state":"utah","zip":"14180"},"email":"bernard.hansen84@example.com","username":"whiteostrich230","password":"watcher","salt":"3AqU8gaa","md5":"ed74e0032f667f548bf5e07e2c7fbc95","sha1":"de2bbcb497ee22b2415591d6a4ff8653eee121fa","sha256":"113f4c41b00a6f9ddd25e63e041a4a6ea3499faea596f022aace0cdc73a0aa62","registered":"1222115051","dob":"70643971","phone":"(779)-403-3421","cell":"(395)-693-5259","SSN":"432-47-7036","picture":{"large":"http://api.randomuser.me/portraits/men/89.jpg","medium":"http://api.randomuser.me/portraits/med/men/89.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/89.jpg"},"version":"0.5","nationality":"US"},"seed":"0130fa838cfa8fb1"},{"user":{"gender":"female","name":{"title":"mrs","first":"teresa","last":"horton"},"location":{"street":"1762 james st","city":"columbus","state":"arkansas","zip":"22910"},"email":"teresa.horton29@example.com","username":"bluemeercat400","password":"target","salt":"MRyZZU7k","md5":"033e8dfdcbc52cd5b840dbab021d3f35","sha1":"271cd57d7e7d6b3ba0391142827f9edddd701af3","sha256":"1c7ec3ea8514a048256896dac2fc55ab5a37f23a5090aa248703ce92730d156a","registered":"1097009927","dob":"144371897","phone":"(831)-172-5647","cell":"(952)-152-1514","SSN":"131-20-1225","picture":{"large":"http://api.randomuser.me/portraits/women/17.jpg","medium":"http://api.randomuser.me/portraits/med/women/17.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/17.jpg"},"version":"0.5","nationality":"US"},"seed":"f8c0cfdf4ccfe1ad"},{"user":{"gender":"female","name":{"title":"ms","first":"addison","last":"oliver"},"location":{"street":"3284 camden ave","city":"addison","state":"maryland","zip":"75550"},"email":"addison.oliver68@example.com","username":"ticklishpeacock120","password":"treetop","salt":"F5uaQtxL","md5":"bcadca84359f44e5c3dc7b7c5dea37f6","sha1":"60bdeffc25c9db962a122d24982acec6ded2bf34","sha256":"b3c781aefad0505648c4f80d019a3f6117ee750719c7671a672f0695db96a39b","registered":"1165561429","dob":"215028172","phone":"(297)-554-6267","cell":"(140)-845-3732","SSN":"679-13-8763","picture":{"large":"http://api.randomuser.me/portraits/women/70.jpg","medium":"http://api.randomuser.me/portraits/med/women/70.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/70.jpg"},"version":"0.5","nationality":"US"},"seed":"e5ff8885a07ce17b"},{"user":{"gender":"female","name":{"title":"mrs","first":"kathryn","last":"edwards"},"location":{"street":"3508 hogan st","city":"bernalillo","state":"maryland","zip":"12557"},"email":"kathryn.edwards52@example.com","username":"crazybutterfly119","password":"truman","salt":"8nvMqws5","md5":"4266d96d99e175fa525bce20af312374","sha1":"64087ca486b5e0fbdd02440175168024ecbb307d","sha256":"bd0ad8094deeb2b8d3134237c0ef495bc74eb3490b9c5344f1ef2928ae4ed233","registered":"1111907972","dob":"53115784","phone":"(161)-557-2707","cell":"(824)-226-8372","SSN":"928-34-7953","picture":{"large":"http://api.randomuser.me/portraits/women/85.jpg","medium":"http://api.randomuser.me/portraits/med/women/85.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/85.jpg"},"version":"0.5","nationality":"US"},"seed":"417791898ef425ed"},{"user":{"gender":"male","name":{"title":"mr","first":"clinton","last":"willis"},"location":{"street":"5838 cherry st","city":"eureka","state":"wyoming","zip":"27848"},"email":"clinton.willis42@example.com","username":"yellowbird481","password":"combat","salt":"slUWHR6E","md5":"5dab830c99afacce2d5a187ba07c7834","sha1":"cfea826536628ab327ccc435f14d43b959ceb414","sha256":"51b9f27c5d3de51f2782eb7e69f28a53941776e4073f2dfb6c4cd7e6ccfa76bd","registered":"1264420879","dob":"497311290","phone":"(657)-279-4046","cell":"(268)-983-4671","SSN":"354-49-7468","picture":{"large":"http://api.randomuser.me/portraits/men/69.jpg","medium":"http://api.randomuser.me/portraits/med/men/69.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/69.jpg"},"version":"0.5","nationality":"US"},"seed":"2f059f1a301430d7"},{"user":{"gender":"female","name":{"title":"miss","first":"evelyn","last":"fernandez"},"location":{"street":"8223 washington ave","city":"denver","state":"north carolina","zip":"61829"},"email":"evelyn.fernandez26@example.com","username":"silvermouse497","password":"grass","salt":"0IsgnWAg","md5":"e67019a3120a11e3b21a2895d2a1ea5e","sha1":"3d5e0e021bb4876633b47220740698e93a53b686","sha256":"33c0821c290b5a2e00aea5bde84fbddde5b6e4befa3bec8f1a75d03024d81201","registered":"1058411483","dob":"126989083","phone":"(685)-436-3493","cell":"(410)-670-4291","SSN":"331-49-1805","picture":{"large":"http://api.randomuser.me/portraits/women/40.jpg","medium":"http://api.randomuser.me/portraits/med/women/40.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/40.jpg"},"version":"0.5","nationality":"US"},"seed":"99fc1e5d519b9521"},{"user":{"gender":"female","name":{"title":"ms","first":"taylor","last":"richards"},"location":{"street":"5336 hillcrest rd","city":"lansing","state":"new york","zip":"53026"},"email":"taylor.richards79@example.com","username":"beautifulfish550","password":"summer1","salt":"CwBj6UI0","md5":"581a8b9f34ffbabc28f116de063f0f19","sha1":"c78451ec31a0514cb332eefa753d1efe4fc62fa8","sha256":"da2294e39f8e27a6df44454449db03fb698bbe3bf5f65c72d33ea7c323082718","registered":"962830541","dob":"96312330","phone":"(124)-636-4123","cell":"(623)-751-3945","SSN":"398-85-4608","picture":{"large":"http://api.randomuser.me/portraits/women/59.jpg","medium":"http://api.randomuser.me/portraits/med/women/59.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/59.jpg"},"version":"0.5","nationality":"US"},"seed":"867d9a29a86ae44a"},{"user":{"gender":"female","name":{"title":"miss","first":"gertrude","last":"lawson"},"location":{"street":"6364 timber wolf trail","city":"forney","state":"idaho","zip":"94349"},"email":"gertrude.lawson55@example.com","username":"ticklishsnake424","password":"bananas","salt":"iwaRkaiw","md5":"5cb740a7bc7a65672b76b02a8f3929d4","sha1":"589e9e6fc154336b2b375de552694f0cab46e38e","sha256":"a791c6203f55bd498593452f9b0127ffbc526cb327cd083ec4b6f70f76ca4de7","registered":"1236839842","dob":"444126443","phone":"(579)-300-1080","cell":"(425)-810-3192","SSN":"752-20-4071","picture":{"large":"http://api.randomuser.me/portraits/women/62.jpg","medium":"http://api.randomuser.me/portraits/med/women/62.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/62.jpg"},"version":"0.5","nationality":"US"},"seed":"6d86973a62bef06a"},{"user":{"gender":"female","name":{"title":"mrs","first":"terri","last":"stewart"},"location":{"street":"7432 shady ln dr","city":"bernalillo","state":"illinois","zip":"24192"},"email":"terri.stewart87@example.com","username":"lazypanda972","password":"powder","salt":"bPZwO09B","md5":"8a07978f858399934ebb107ef099ef1d","sha1":"e3230501169d7d07d42b97236857a0c74a6fd538","sha256":"102779c88fe9a744b01400576155a2be59ac9ca3f4b1b1e1acc66b9c6fa07dfe","registered":"1042663036","dob":"247819139","phone":"(244)-806-2574","cell":"(744)-233-2902","SSN":"595-73-9020","picture":{"large":"http://api.randomuser.me/portraits/women/47.jpg","medium":"http://api.randomuser.me/portraits/med/women/47.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/47.jpg"},"version":"0.5","nationality":"US"},"seed":"f0ef2e8e06f5fc23"},{"user":{"gender":"male","name":{"title":"mr","first":"gary","last":"rivera"},"location":{"street":"9614 college st","city":"billings","state":"california","zip":"56908"},"email":"gary.rivera67@example.com","username":"redlion378","password":"golfer","salt":"azS8mQ63","md5":"9fea690968cc3bd9c1433988c09929dd","sha1":"55ca0243acdb233e5488d47d2361a0c32c007b1f","sha256":"b638204384676e99087d8530f1d64c5491eb7b89258cabb3f0b439b9db277831","registered":"1075685529","dob":"51907909","phone":"(740)-845-5073","cell":"(972)-568-9373","SSN":"449-87-8819","picture":{"large":"http://api.randomuser.me/portraits/men/64.jpg","medium":"http://api.randomuser.me/portraits/med/men/64.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/64.jpg"},"version":"0.5","nationality":"US"},"seed":"713b9b5ffbf374e8"},{"user":{"gender":"female","name":{"title":"ms","first":"sharlene","last":"washington"},"location":{"street":"4789 elgin st","city":"celina","state":"maine","zip":"71490"},"email":"sharlene.washington55@example.com","username":"yellowsnake819","password":"wwww","salt":"m4n5CrRW","md5":"80050ca594242c3263ba9c070ae9066b","sha1":"60c1376e9534cd5b7ebc063b32e28db2a7300a05","sha256":"2ea3d816d07bcfae89838f9cd1c1e28b2d4a91584a1a031290d5657f9a8f4208","registered":"1362919552","dob":"3435882","phone":"(529)-414-8924","cell":"(883)-667-3095","SSN":"453-12-1020","picture":{"large":"http://api.randomuser.me/portraits/women/28.jpg","medium":"http://api.randomuser.me/portraits/med/women/28.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/28.jpg"},"version":"0.5","nationality":"US"},"seed":"baba79673333f1e2"},{"user":{"gender":"female","name":{"title":"ms","first":"chloe","last":"lawrence"},"location":{"street":"8005 james st","city":"stanley","state":"alabama","zip":"62184"},"email":"chloe.lawrence29@example.com","username":"orangegoose360","password":"spectre","salt":"Hrp0SKtD","md5":"321f0214d398e9ae593a6e3124b72c66","sha1":"d83ebb162e1477219c782aceeb107613c08cb000","sha256":"a20d18b097251c4a1f8c13289db4340c73483aa8a16c3f6a7b14074021a58032","registered":"1173466864","dob":"302098884","phone":"(544)-844-3480","cell":"(149)-473-4582","SSN":"926-83-4478","picture":{"large":"http://api.randomuser.me/portraits/women/89.jpg","medium":"http://api.randomuser.me/portraits/med/women/89.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/89.jpg"},"version":"0.5","nationality":"US"},"seed":"948b9e5e03d04417"},{"user":{"gender":"female","name":{"title":"mrs","first":"dana","last":"woods"},"location":{"street":"5596 mcclellan rd","city":"los angeles","state":"hawaii","zip":"51038"},"email":"dana.woods71@example.com","username":"orangedog222","password":"kendall","salt":"qKReEnRu","md5":"34efc8441fe3471c345be38540e2eb6d","sha1":"8a34159426ffe7a2747cfc2806bb7c57760882a7","sha256":"17d172415e3a7b59a1fe8934159ca60e392981beceabd8880fb8ede3b9f1707f","registered":"1302668483","dob":"257293285","phone":"(859)-916-9748","cell":"(336)-384-9002","SSN":"828-17-3395","picture":{"large":"http://api.randomuser.me/portraits/women/52.jpg","medium":"http://api.randomuser.me/portraits/med/women/52.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/52.jpg"},"version":"0.5","nationality":"US"},"seed":"bf87554519dfed48"},{"user":{"gender":"female","name":{"title":"mrs","first":"erin","last":"gonzales"},"location":{"street":"3465 dane st","city":"albany","state":"west virginia","zip":"98505"},"email":"erin.gonzales47@example.com","username":"bigelephant681","password":"darren","salt":"Dk8KaJQp","md5":"1fe314e8b4272e3f71b9b0b20709e8c1","sha1":"fc170062ec38575b0499f3f9099dc8a6376128b9","sha256":"64d7bd302d71df4ed91997d80f95e6394157b244ccea03342f7a24e18fe687bb","registered":"1382146677","dob":"426870108","phone":"(661)-743-4657","cell":"(872)-975-6945","SSN":"677-92-1257","picture":{"large":"http://api.randomuser.me/portraits/women/29.jpg","medium":"http://api.randomuser.me/portraits/med/women/29.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/29.jpg"},"version":"0.5","nationality":"US"},"seed":"01f7ee09a53c1b9b"},{"user":{"gender":"male","name":{"title":"mr","first":"noah","last":"barrett"},"location":{"street":"7949 parker rd","city":"memphis","state":"massachusetts","zip":"40616"},"email":"noah.barrett79@example.com","username":"purplelion871","password":"success","salt":"ug0hsgkq","md5":"5e9eb22e4ccaa29092832aea82e4953f","sha1":"5e18a323015689665e27efc93c693337e1263f55","sha256":"2e543d19103ae6051632ba20607ab2883fa3374a584a8c9a1007a80822ff07cc","registered":"1239622620","dob":"291018523","phone":"(321)-881-7107","cell":"(741)-732-4091","SSN":"232-81-7320","picture":{"large":"http://api.randomuser.me/portraits/men/48.jpg","medium":"http://api.randomuser.me/portraits/med/men/48.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/48.jpg"},"version":"0.5","nationality":"US"},"seed":"31dc86939de8ac35"},{"user":{"gender":"male","name":{"title":"mr","first":"tyler","last":"wright"},"location":{"street":"7408 locust rd","city":"celina","state":"delaware","zip":"71108"},"email":"tyler.wright38@example.com","username":"heavyelephant639","password":"cang","salt":"bFoWARY5","md5":"bc3998f94258a8d84413aa49e7070952","sha1":"9b09d0d035a939bc27d0bfa6d1c983c694601218","sha256":"06b71efb6e34d61f681ab7eeafe4d7fdf94dabc2d0f78e6eb898a752988193d5","registered":"1005273834","dob":"394648761","phone":"(523)-144-4155","cell":"(647)-399-5561","SSN":"426-16-1175","picture":{"large":"http://api.randomuser.me/portraits/men/35.jpg","medium":"http://api.randomuser.me/portraits/med/men/35.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/35.jpg"},"version":"0.5","nationality":"US"},"seed":"4da826a841380ceb"},{"user":{"gender":"female","name":{"title":"miss","first":"joanne","last":"graham"},"location":{"street":"3572 green rd","city":"rochester","state":"new jersey","zip":"55688"},"email":"joanne.graham28@example.com","username":"heavyelephant872","password":"reptile","salt":"aJIXefbS","md5":"ad0d3e737a8437392737b8e9cfbf64d1","sha1":"597c7f0a677d3cfd0714deb57317cc976dc33174","sha256":"025c255d80d7b29d604e1381e3ef6fb6d3670b7bde75ecfd3049f235d6a7b53d","registered":"1312449547","dob":"180893164","phone":"(821)-574-1053","cell":"(832)-282-4417","SSN":"362-10-8539","picture":{"large":"http://api.randomuser.me/portraits/women/77.jpg","medium":"http://api.randomuser.me/portraits/med/women/77.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/77.jpg"},"version":"0.5","nationality":"US"},"seed":"d47d36c3c167e251"},{"user":{"gender":"male","name":{"title":"mr","first":"lee","last":"morgan"},"location":{"street":"1205 w campbell ave","city":"utica","state":"california","zip":"36751"},"email":"lee.morgan97@example.com","username":"beautifulgorilla501","password":"beatle","salt":"sWCWhSuE","md5":"84889446feb3c11f091e5e1f1389c574","sha1":"24e3ca8e06f85a1888066302777d2927b25f6e24","sha256":"690a3f3925dfec708f5bcc2060af2502aa0358c2f64ac94d4b55c38b804e2b49","registered":"1378071625","dob":"155147333","phone":"(691)-751-5654","cell":"(193)-738-5390","SSN":"173-34-1917","picture":{"large":"http://api.randomuser.me/portraits/men/42.jpg","medium":"http://api.randomuser.me/portraits/med/men/42.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/42.jpg"},"version":"0.5","nationality":"US"},"seed":"3b0b73f9ba4e81a7"},{"user":{"gender":"female","name":{"title":"mrs","first":"mabel","last":"perry"},"location":{"street":"2289 w belt line rd","city":"ironville","state":"mississippi","zip":"66469"},"email":"mabel.perry76@example.com","username":"redlion38","password":"workout","salt":"oAh6PsFf","md5":"269bacdd7c7aa92c53df2887c63139b3","sha1":"7880dfc901438ce5c9cf51ef7a6688b2163b00d9","sha256":"43153d02fc28885b3d9843a5a3802e02abbfb7d78eaf0268701b4867406c5075","registered":"1221726260","dob":"264915954","phone":"(205)-839-6749","cell":"(682)-118-1158","SSN":"782-53-6749","picture":{"large":"http://api.randomuser.me/portraits/women/21.jpg","medium":"http://api.randomuser.me/portraits/med/women/21.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/21.jpg"},"version":"0.5","nationality":"US"},"seed":"39ddf798e8fa1a24"},{"user":{"gender":"female","name":{"title":"ms","first":"harper","last":"morris"},"location":{"street":"2253 valwood pkwy","city":"frisco","state":"wisconsin","zip":"57453"},"email":"harper.morris65@example.com","username":"ticklishswan352","password":"thethe","salt":"liYwm0nn","md5":"602c73178600cc8c855b992037c099e3","sha1":"dcadde08963ad8150b324f92d6663d36f7934260","sha256":"09acdc68523f0f232073fcef6575e6b0629e69cc62103e0e69dc92106953eea4","registered":"971886510","dob":"337578787","phone":"(760)-756-9855","cell":"(483)-353-7362","SSN":"803-89-1639","picture":{"large":"http://api.randomuser.me/portraits/women/84.jpg","medium":"http://api.randomuser.me/portraits/med/women/84.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/84.jpg"},"version":"0.5","nationality":"US"},"seed":"054436effe4b7c5c"},{"user":{"gender":"male","name":{"title":"mr","first":"philip","last":"ellis"},"location":{"street":"7542 spring st","city":"cincinnati","state":"new hampshire","zip":"38656"},"email":"philip.ellis58@example.com","username":"yellowbird16","password":"matteo","salt":"uG7vvzHS","md5":"5ce0fafc901265fde3824198a2b6f978","sha1":"116ae31e982a8aad6120ef6b61dcea09c0497626","sha256":"9ec807fb300f3298565b17b21978005b9e937ba6b19f469d540ba6cbcc6ffeb6","registered":"1306840795","dob":"35001358","phone":"(666)-208-2234","cell":"(188)-817-8054","SSN":"924-69-8009","picture":{"large":"http://api.randomuser.me/portraits/men/70.jpg","medium":"http://api.randomuser.me/portraits/med/men/70.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/70.jpg"},"version":"0.5","nationality":"US"},"seed":"0a0d9877092b629a"},{"user":{"gender":"male","name":{"title":"mr","first":"kent","last":"porter"},"location":{"street":"3113 oak lawn ave","city":"great falls","state":"connecticut","zip":"26497"},"email":"kent.porter40@example.com","username":"silverpeacock245","password":"cheater","salt":"uXoomLGh","md5":"a2a2765b986dbe3bfe87e31f81648c19","sha1":"285b7911b3d5352a532f036b09e5f519b0d5aae8","sha256":"e08c6459aa3ce945a436a5be9362e0e7dd897272bea1e6d593abcb205e3c47a3","registered":"1224107117","dob":"116672955","phone":"(255)-886-8876","cell":"(856)-838-5979","SSN":"128-63-6705","picture":{"large":"http://api.randomuser.me/portraits/men/65.jpg","medium":"http://api.randomuser.me/portraits/med/men/65.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/65.jpg"},"version":"0.5","nationality":"US"},"seed":"2e5e364338f8d884"},{"user":{"gender":"male","name":{"title":"mr","first":"willie","last":"castro"},"location":{"street":"4770 plum st","city":"nampa","state":"florida","zip":"39488"},"email":"willie.castro84@example.com","username":"blackgorilla260","password":"thuglife","salt":"PfHMj9UP","md5":"f9220d8b76a1f417a0759b65b39ef899","sha1":"24c952a7551ba768a7f94f12979eb085f3209511","sha256":"b5172d3e7a02a315596fddc1e67730a43d56a5096e48631e369ed6f4d44d5cda","registered":"1277977842","dob":"342462483","phone":"(744)-936-9661","cell":"(814)-816-3707","SSN":"722-81-7523","picture":{"large":"http://api.randomuser.me/portraits/men/60.jpg","medium":"http://api.randomuser.me/portraits/med/men/60.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/60.jpg"},"version":"0.5","nationality":"US"},"seed":"89cf2228a7136b17"},{"user":{"gender":"female","name":{"title":"mrs","first":"lisa","last":"matthews"},"location":{"street":"1970 miller ave","city":"eugene","state":"vermont","zip":"67293"},"email":"lisa.matthews92@example.com","username":"heavybear880","password":"limited","salt":"ffU7vOZ3","md5":"ceb0b24539368fff253ee5c8be9f6e08","sha1":"320864c59114d8ffc6f650d6a2b6bfd77a657f29","sha256":"657460047db23a42551ec417aeeacdd6562d5c1650b97e558bf33927507f7300","registered":"995410585","dob":"167379935","phone":"(371)-331-3857","cell":"(372)-675-8615","SSN":"432-45-7800","picture":{"large":"http://api.randomuser.me/portraits/women/63.jpg","medium":"http://api.randomuser.me/portraits/med/women/63.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/63.jpg"},"version":"0.5","nationality":"US"},"seed":"eda6dcd9d6e9cd9e"},{"user":{"gender":"male","name":{"title":"mr","first":"harold","last":"porter"},"location":{"street":"4983 wycliff ave","city":"roanoke","state":"north dakota","zip":"27494"},"email":"harold.porter57@example.com","username":"blueleopard511","password":"clifton","salt":"aawd2GC3","md5":"15c68bc46a42bf1037356769c9b385f0","sha1":"13ad50c17369ad1ab291820adb2a9ac4640930ca","sha256":"debfbfd3064bf41a4720459d7d73397743c951e2809440d6e1f5374f28e438ba","registered":"1320432459","dob":"33577962","phone":"(837)-645-7890","cell":"(203)-500-7547","SSN":"564-50-8350","picture":{"large":"http://api.randomuser.me/portraits/men/42.jpg","medium":"http://api.randomuser.me/portraits/med/men/42.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/42.jpg"},"version":"0.5","nationality":"US"},"seed":"5319a30ce426aaf1"},{"user":{"gender":"male","name":{"title":"mr","first":"greg","last":"bryant"},"location":{"street":"7926 rolling green rd","city":"columbus","state":"new york","zip":"32996"},"email":"greg.bryant32@example.com","username":"orangedog180","password":"pacers","salt":"5NaL4iYZ","md5":"68fd758081db06253db2375816791b65","sha1":"9d03aa420b0f6b5bcf27b92e64713e09cfa1362e","sha256":"e38776e7a7760d3aa406198cdd0e0fea4d0f5891fa82f47f1d96b39bfd9388df","registered":"1324663642","dob":"163180112","phone":"(825)-277-7249","cell":"(297)-803-1998","SSN":"377-76-9240","picture":{"large":"http://api.randomuser.me/portraits/men/84.jpg","medium":"http://api.randomuser.me/portraits/med/men/84.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/84.jpg"},"version":"0.5","nationality":"US"},"seed":"1c4375eebdf99c97"},{"user":{"gender":"female","name":{"title":"ms","first":"vanessa","last":"holland"},"location":{"street":"9669 washington ave","city":"new haven","state":"alabama","zip":"23658"},"email":"vanessa.holland94@example.com","username":"whiteladybug675","password":"99999","salt":"Rcz9US9g","md5":"160b5c8b7ee543a7117130e4a162b28f","sha1":"3e99aac6ee7350be80b8b160f87f570937ed69be","sha256":"58adddc2653c46b41718b9ae576ea14ca9f8b22b69a8d8480e0d37adc2b89798","registered":"920746097","dob":"185662756","phone":"(231)-752-5353","cell":"(978)-214-2816","SSN":"462-58-4861","picture":{"large":"http://api.randomuser.me/portraits/women/73.jpg","medium":"http://api.randomuser.me/portraits/med/women/73.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/73.jpg"},"version":"0.5","nationality":"US"},"seed":"08da90afb5b60711"},{"user":{"gender":"male","name":{"title":"mr","first":"angel","last":"bailey"},"location":{"street":"4058 oak ridge ln","city":"grand prairie","state":"alabama","zip":"84118"},"email":"angel.bailey86@example.com","username":"ticklishbird516","password":"sugar","salt":"WUGm6a1V","md5":"2fb4146a5800cef5d76c11034b9d383f","sha1":"ff0fb87fb3e8d61ce4a65ca39245fe335ddc3b3a","sha256":"063719660b62de6269eacb4b6e01d7bf011d59ac957b8ec37aa6f0032c71fcdf","registered":"1389188477","dob":"149710596","phone":"(942)-184-7486","cell":"(402)-805-3872","SSN":"604-21-1072","picture":{"large":"http://api.randomuser.me/portraits/men/25.jpg","medium":"http://api.randomuser.me/portraits/med/men/25.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/25.jpg"},"version":"0.5","nationality":"US"},"seed":"b8e35001ebae125b"},{"user":{"gender":"male","name":{"title":"mr","first":"jack","last":"davidson"},"location":{"street":"8288 thornridge cir","city":"billings","state":"kansas","zip":"42553"},"email":"jack.davidson31@example.com","username":"organicpeacock533","password":"domino","salt":"X4mG1YAe","md5":"b25b64b25533be3fb752814220845209","sha1":"d82c12be56a12b8a6ebfda4726e9780872066d69","sha256":"bf8a7bae98872d099d5551642a71856622ee4c82d3e36b965408d077e3dcdf1f","registered":"1421542334","dob":"351295483","phone":"(514)-386-8012","cell":"(814)-828-7729","SSN":"307-70-1987","picture":{"large":"http://api.randomuser.me/portraits/men/33.jpg","medium":"http://api.randomuser.me/portraits/med/men/33.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/33.jpg"},"version":"0.5","nationality":"US"},"seed":"9dc70b4ffb0c1b4c"},{"user":{"gender":"male","name":{"title":"mr","first":"glen","last":"vargas"},"location":{"street":"1077 green rd","city":"the colony","state":"wisconsin","zip":"18517"},"email":"glen.vargas89@example.com","username":"blacksnake270","password":"chou","salt":"eGwIAfbC","md5":"195aa17c3f5ce13d804ccbc5ae4594a5","sha1":"a4eb6cff225d346f0ed86be4820f2dd4d1e9b3f3","sha256":"f01f8e15afe98b3323acd420693ed7ee8414aa5bbe8f75a036831b2935211ef8","registered":"1219520476","dob":"11885480","phone":"(899)-179-5328","cell":"(561)-475-1645","SSN":"258-86-8039","picture":{"large":"http://api.randomuser.me/portraits/men/55.jpg","medium":"http://api.randomuser.me/portraits/med/men/55.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/55.jpg"},"version":"0.5","nationality":"US"},"seed":"26da25e2f1bec4b2"},{"user":{"gender":"male","name":{"title":"mr","first":"lawrence","last":"hicks"},"location":{"street":"2596 thornridge cir","city":"new haven","state":"pennsylvania","zip":"76502"},"email":"lawrence.hicks88@example.com","username":"organicbutterfly971","password":"musical","salt":"u4VdtHMe","md5":"d13cad28ccf3910e9e08e7734e0988b0","sha1":"f518c84d996ea6c495aa63fe8b6fe95be1ed07b0","sha256":"74956918d06b9cce442c7bfac0bc7b5511b7635d138f0bacd7b65b6e8600c8b8","registered":"1243830469","dob":"267189339","phone":"(212)-613-3261","cell":"(207)-804-9630","SSN":"934-56-6111","picture":{"large":"http://api.randomuser.me/portraits/men/72.jpg","medium":"http://api.randomuser.me/portraits/med/men/72.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/72.jpg"},"version":"0.5","nationality":"US"},"seed":"c33baacdb77da37e"},{"user":{"gender":"male","name":{"title":"mr","first":"mathew","last":"lewis"},"location":{"street":"4491 cackson st","city":"new york","state":"colorado","zip":"12286"},"email":"mathew.lewis36@example.com","username":"greenfrog909","password":"native","salt":"6vHoUtaQ","md5":"967a9673b02fcd5d0c5f479c1b15b63c","sha1":"1f532b5b90f1dd3fdcbf1f2871767d98e9e88fd5","sha256":"de8e04985c8e293907f4a3db57f91caef52570446963057e8e5293aa6b39bfb6","registered":"1046300144","dob":"253388810","phone":"(950)-708-2817","cell":"(237)-984-6418","SSN":"839-48-8112","picture":{"large":"http://api.randomuser.me/portraits/men/56.jpg","medium":"http://api.randomuser.me/portraits/med/men/56.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/56.jpg"},"version":"0.5","nationality":"US"},"seed":"ec0d38a51851ddbc"},{"user":{"gender":"male","name":{"title":"mr","first":"isaiah","last":"spencer"},"location":{"street":"7998 fairview st","city":"red bluff","state":"louisiana","zip":"87232"},"email":"isaiah.spencer59@example.com","username":"greenostrich26","password":"yummy","salt":"heSHfZow","md5":"b2b24e06c634d89ab09e13fa2787abee","sha1":"e2d4893e5fe42aa13d5503c926b0c8cc1840e174","sha256":"e13673bd1ee865001c3f08e58abf72ca6e7029b38784cc3a3ecd5d554763fde2","registered":"1030192542","dob":"300670646","phone":"(559)-704-3855","cell":"(877)-667-4231","SSN":"827-78-4601","picture":{"large":"http://api.randomuser.me/portraits/men/52.jpg","medium":"http://api.randomuser.me/portraits/med/men/52.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/52.jpg"},"version":"0.5","nationality":"US"},"seed":"35e03412ca94150c"},{"user":{"gender":"female","name":{"title":"mrs","first":"samantha","last":"brewer"},"location":{"street":"4168 depaul dr","city":"wichita falls","state":"idaho","zip":"60028"},"email":"samantha.brewer73@example.com","username":"beautifulrabbit189","password":"gerbil","salt":"ARBFhlWA","md5":"a5a6c0b847fa6f195f5e3a1f5f8f03df","sha1":"26b0d842e89f5e7a710d0f9e2d35bf53bc780aeb","sha256":"e3aa27fa1159e04e61d95164029a8d6bf4dc3ceb2e2c70a1fc70f2104f953ff3","registered":"1276104566","dob":"352143758","phone":"(774)-198-4935","cell":"(899)-904-1918","SSN":"504-40-2645","picture":{"large":"http://api.randomuser.me/portraits/women/16.jpg","medium":"http://api.randomuser.me/portraits/med/women/16.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/16.jpg"},"version":"0.5","nationality":"US"},"seed":"64a481768708fd71"},{"user":{"gender":"female","name":{"title":"mrs","first":"sofia","last":"jenkins"},"location":{"street":"4234 spring st","city":"frisco","state":"vermont","zip":"13274"},"email":"sofia.jenkins24@example.com","username":"lazyswan422","password":"fergus","salt":"fAWb2tqu","md5":"a093212a0b035253968fc1a99744fb35","sha1":"6df049fc0470e29457cddd1480811aa5b59b7d77","sha256":"74ade535b8581465345cd4dee2d1492ea0f209ad274fb135c1f571ed5e1b4039","registered":"963941181","dob":"238210834","phone":"(561)-296-5029","cell":"(727)-411-1099","SSN":"194-62-7048","picture":{"large":"http://api.randomuser.me/portraits/women/2.jpg","medium":"http://api.randomuser.me/portraits/med/women/2.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/2.jpg"},"version":"0.5","nationality":"US"},"seed":"5ddeed288d93a79f"},{"user":{"gender":"male","name":{"title":"mr","first":"james","last":"hanson"},"location":{"street":"7273 e sandy lake rd","city":"lewiston","state":"ohio","zip":"41902"},"email":"james.hanson44@example.com","username":"brownfrog774","password":"moscow","salt":"b7gasTHT","md5":"ea1483ead0bb1a9e2aeffeaf84aa5f59","sha1":"b9e8d9a7da7316656c621a0101d164917b01d223","sha256":"c88e5a9f6b246b4c34ccf6f9aee30f5198bd396365c06e206bfb8f611674448b","registered":"1257209836","dob":"138429521","phone":"(315)-928-9525","cell":"(151)-552-5000","SSN":"559-70-2362","picture":{"large":"http://api.randomuser.me/portraits/men/12.jpg","medium":"http://api.randomuser.me/portraits/med/men/12.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/men/12.jpg"},"version":"0.5","nationality":"US"},"seed":"bd5ccdf409e37840"},{"user":{"gender":"female","name":{"title":"ms","first":"marilyn","last":"ryan"},"location":{"street":"9012 harrison ct","city":"burkburnett","state":"colorado","zip":"13560"},"email":"marilyn.ryan63@example.com","username":"redmeercat415","password":"total","salt":"rYvakgXT","md5":"ed1b692f90e0dffd5f481555a6fcd4ff","sha1":"e1a41d213ff00005ac416981ce6b6a9c3e1bf450","sha256":"b55358ff3fe5d542559c6a64c07caecdce6a7b74a9b356187c2fb250d48d6a43","registered":"1213059456","dob":"152360440","phone":"(443)-281-6808","cell":"(959)-456-9322","SSN":"310-91-6475","picture":{"large":"http://api.randomuser.me/portraits/women/4.jpg","medium":"http://api.randomuser.me/portraits/med/women/4.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/4.jpg"},"version":"0.5","nationality":"US"},"seed":"ea9297b3c77af691"},{"user":{"gender":"female","name":{"title":"mrs","first":"bertha","last":"flores"},"location":{"street":"9171 white oak dr","city":"seattle","state":"rhode island","zip":"91478"},"email":"bertha.flores11@example.com","username":"heavywolf852","password":"mouth","salt":"sp0iulKU","md5":"69f4b8680aa850e88bab11dba045f4ba","sha1":"9135b08289decba6ca507aee65f5d810df6b9cb6","sha256":"71fb4c406427857e93ce489ee38bc549af042d31987297cf6eb90e4b799d4ba4","registered":"930156204","dob":"479679322","phone":"(215)-824-6438","cell":"(797)-939-8521","SSN":"499-87-9587","picture":{"large":"http://api.randomuser.me/portraits/women/43.jpg","medium":"http://api.randomuser.me/portraits/med/women/43.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/43.jpg"},"version":"0.5","nationality":"US"},"seed":"bc970f42c34e9bfb"},{"user":{"gender":"female","name":{"title":"ms","first":"brianna","last":"scott"},"location":{"street":"3540 camden ave","city":"moscow","state":"arizona","zip":"89362"},"email":"brianna.scott40@example.com","username":"purplelion428","password":"phillips","salt":"3W97LISl","md5":"04a3b2db411298fea35b754e6366e5bd","sha1":"cdf412be84e5e3e5e41a10ec9989ca53201f091c","sha256":"e6128cdd9a776f09e5c867d2d0caabff0a726946287c9c2cd2ecd2d7b8bd9a8b","registered":"1184788976","dob":"233031190","phone":"(469)-447-4542","cell":"(277)-929-2425","SSN":"720-34-2300","picture":{"large":"http://api.randomuser.me/portraits/women/59.jpg","medium":"http://api.randomuser.me/portraits/med/women/59.jpg","thumbnail":"http://api.randomuser.me/portraits/thumb/women/59.jpg"},"version":"0.5","nationality":"US"},"seed":"e3f05a301d8a3d91"}]} 2 | -------------------------------------------------------------------------------- /server/api/routes.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { readFile } from 'fs-promise'; 3 | 4 | import marked from 'marked'; 5 | 6 | import { users } from './data.json'; 7 | 8 | const simplifyUsers = (collection) => collection 9 | .map(({ user, seed }) => ({ ...user, seed })) 10 | .map(({ name, seed, picture }) => ({ name, seed, picture })); 11 | 12 | export default function(router) { 13 | router.get('/users', function(req, res) { 14 | const results = simplifyUsers(users.slice(0, 10)); 15 | return res.status(200).send(results); 16 | }); 17 | 18 | router.get('/users/:seed', function(req, res) { 19 | const { seed } = req.params; 20 | const [ result ] = simplifyUsers(users.filter(user => user.seed === seed)); 21 | 22 | if (!result) return res.status(422).send({ error: { message: 'User not found' } }); 23 | return res.status(200).send(result); 24 | }); 25 | 26 | router.get('/readme', async function(req, res) { 27 | const readme = await readFile(path.resolve(__dirname, '../../README.md'), 'utf8'); 28 | return res.status(200).send(marked(readme)); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /server/express.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import debug from 'debug'; 3 | import express from 'express'; 4 | import helmet from 'helmet'; 5 | 6 | import createLocation from 'history/lib/createLocation'; 7 | 8 | import ApiClient from '../shared/api-client'; 9 | import universalRender from '../shared/universal-render'; 10 | 11 | import createStore from 'redux/create'; 12 | 13 | const { NODE_ENV = 'development', PORT = 3000 } = process.env; 14 | const server = express(); 15 | 16 | if (NODE_ENV !== 'production') { 17 | debug.enable('dev,server'); 18 | } else { 19 | debug.enable('server'); 20 | } 21 | 22 | // expressjs middlewares 23 | server.use(require('response-time')()); 24 | server.use(require('morgan')('tiny')); 25 | 26 | // helmet middlewares / security 27 | server.use(helmet.xframe()); 28 | server.use(helmet.xssFilter()); 29 | server.use(helmet.nosniff()); 30 | server.use(helmet.ienoopen()); 31 | server.disable('x-powered-by'); 32 | 33 | // enable body parser 34 | server.use(require('body-parser').json()); 35 | 36 | // Should be placed before express.static 37 | server.use(require('compression')({ 38 | // only compress files for the following content types 39 | filter: function(req, res) { 40 | return (/json|text|javascript|css/) 41 | .test(res.getHeader('Content-Type')); 42 | }, 43 | // zlib option for compression level 44 | level: 3 45 | })); 46 | 47 | // serve favicon 48 | server.use(require('serve-favicon')(path.resolve(__dirname, '../app/images/favicon.ico'))); 49 | 50 | server.use('/assets', express.static(path.resolve(__dirname, '../dist'))); 51 | server.set('views', path.resolve(__dirname, 'views')); 52 | server.set('view engine', 'ejs'); 53 | 54 | // Run requests through api router first 55 | const apiRouter = express.Router(); /* eslint new-cap:0 */ 56 | require('./api/routes')(apiRouter); 57 | server.use('/api', apiRouter); 58 | 59 | // Run requests through react-router next 60 | server.use(async function(req, res) { 61 | try { 62 | // Initialize Redux 63 | const client = new ApiClient(req); 64 | const location = createLocation(req.path, req.query); 65 | const store = createStore(client, {}); 66 | const locale = req.acceptsLanguages(['en', 'fr']) || 'en'; 67 | 68 | const { body, state } = await universalRender({ location, store, client, locale }); 69 | 70 | // Load assets paths from `webpack-stats` 71 | // remove cache on dev env 72 | const assets = require('./webpack-stats.json'); 73 | if (NODE_ENV === 'development') { 74 | delete require.cache[require.resolve('./webpack-stats.json')]; 75 | } 76 | 77 | return res.render('index.ejs', { assets, body, state }); 78 | } catch (error) { 79 | debug('server')('error with rendering'); 80 | debug('server')(error); 81 | 82 | return res.status(500).send(error.stack); 83 | } 84 | }); 85 | 86 | server.listen(PORT); 87 | debug('server')('express server listening on %s', PORT); 88 | 89 | if (process.send) process.send('online'); 90 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Tell `require` calls to look into `/app` also 2 | // it will avoid `../../../../../` require strings 3 | process.env.NODE_PATH = 'app'; 4 | require('module').Module._initPaths(); 5 | 6 | // Install `babel` hook for ES6 7 | require('babel/register'); 8 | 9 | // Intl polyfill 10 | require('intl'); 11 | 12 | // Start the server 13 | require('./express'); 14 | -------------------------------------------------------------------------------- /server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Universal Redux Boilerplate 7 | <%# Stylesheets from `webpack-stats.json` %> 8 | <% assets.style.forEach(function (style) { %> 9 | 10 | <% }); %> 11 | 12 | 13 | <%# React App output %> 14 |
<%- body %>
15 | 16 | 17 | <%# JavaScript from `webpack-stats.json` %> 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /shared/api-client.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const { BROWSER, PORT = 3000 } = process.env; 4 | 5 | class ApiClient { 6 | 7 | constructor(req) { 8 | if (BROWSER) { 9 | this.baseURL = '/api'; 10 | } else { 11 | this.cookie = req.get('cookie'); 12 | this.baseURL = `http://localhost:${PORT}/api`; 13 | } 14 | } 15 | 16 | getConfig(config) { 17 | config.method = config.method || 'get'; 18 | 19 | // Append correct `baseURL` to `config.url` 20 | if (config.baseURL === undefined) { 21 | config.url = config.url ? this.baseURL + config.url : this.baseURL; 22 | } else { 23 | config.url = config.url ? config.baseURL + config.url : config.baseURL; 24 | } 25 | 26 | // Add CORS credentials on browser side 27 | if (BROWSER) { 28 | config.withCredentials = (config.withCredentials === undefined) ? 29 | true : config.withCredentials; 30 | } 31 | 32 | // Copy cookies into headers on server side 33 | if (!BROWSER && this.cookie) config.headers = { cookie: this.cookie }; 34 | 35 | return config; 36 | } 37 | 38 | async request(config = {}) { 39 | try { 40 | const { data } = await axios(this.getConfig(config)); 41 | return data; 42 | } catch (error) { 43 | throw error && error.data || error.stack; 44 | } 45 | } 46 | 47 | } 48 | 49 | export default ApiClient; 50 | -------------------------------------------------------------------------------- /shared/redux-resolver.js: -------------------------------------------------------------------------------- 1 | class ReduxResolver { 2 | 3 | firstRender = true 4 | pendingActions = [] 5 | 6 | resolve(action) { 7 | const [, ...args] = arguments; 8 | if (process.env.BROWSER && !this.firstRender) { 9 | return action(...args); 10 | } else { 11 | this.pendingActions = [ 12 | ...this.pendingActions, 13 | { action, args } 14 | ]; 15 | } 16 | } 17 | 18 | async dispatchPendingActions() { 19 | for (const { action, args } of this.pendingActions) { 20 | await action(...args); 21 | } 22 | } 23 | } 24 | 25 | export default ReduxResolver; 26 | -------------------------------------------------------------------------------- /shared/universal-render.jsx: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/server'; 5 | import { Provider } from 'react-redux'; 6 | import Router, { RoutingContext, match } from 'react-router'; 7 | 8 | import ReduxResolver from './redux-resolver'; 9 | import routes from '../app/routes'; 10 | import * as I18nActions from 'redux/actions/I18nActions'; 11 | 12 | const { BROWSER, NODE_ENV } = process.env; 13 | 14 | const runRouter = (location) => 15 | new Promise((resolve) => 16 | match({ routes, location }, (...args) => resolve(args))); 17 | 18 | /* eslint react/display-name:0 */ 19 | // see: https://github.com/yannickcr/eslint-plugin-react/issues/256 20 | export default async function({ location, history, store, locale }) { 21 | const resolver = new ReduxResolver(); 22 | store.resolver = resolver; 23 | 24 | if (BROWSER && NODE_ENV === 'development') { 25 | // add redux-devtools on client side 26 | const DevTools = require('utils/dev-tools'); 27 | 28 | return ( 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | ); 38 | } else if (BROWSER) { 39 | return ( 40 | 41 | 42 | 43 | ); 44 | } else { 45 | // Initialize locale of rendering 46 | try { 47 | const messages = require(`i18n/${locale}`); 48 | store.dispatch(I18nActions.initialize(locale, messages)); 49 | } catch (error) { 50 | store.dispatch(I18nActions.initialize('en', require('i18n/en'))); 51 | } 52 | 53 | const [ error, redirect, renderProps ] = await runRouter(location); 54 | const routerProps = { ...renderProps, location }; 55 | 56 | // TODO: Fix redirection 57 | if (error || redirect) throw (error || redirect); 58 | 59 | const element = ( 60 | 61 | 62 | 63 | ); 64 | 65 | // Collect promises with a first render 66 | ReactDOM.renderToString(element); 67 | // Resolve them, populate stores 68 | await resolver.dispatchPendingActions(); 69 | // Re-render application with data 70 | const state = serialize(store.getState()); 71 | const body = ReactDOM.renderToString(element); 72 | 73 | return { body, state }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /webpack/base.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | import writeStats from './utils/write-stats'; 5 | 6 | const JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/; 7 | export default { 8 | baseConfig: { 9 | devtool: 'source-map', 10 | entry: { 11 | app: './app/index.js' 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, '../dist'), 15 | filename: '[name]-[hash].js', 16 | chunkFilename: '[name]-[hash].js', 17 | publicPath: '/assets/' 18 | }, 19 | module: { 20 | preLoaders: [ 21 | { test: JS_REGEX, exclude: /node_modules/, loader: 'eslint' } 22 | ], 23 | loaders: [ 24 | { test: /\.json$/, exclude: /node_modules/, loader: 'json' }, 25 | { test: JS_REGEX, exclude: /node_modules/, loader: 'babel' }, 26 | ], 27 | }, 28 | postcss: [ 29 | require('postcss-import')(), 30 | require('postcss-url')(), 31 | require('precss')(), 32 | require('autoprefixer')({ browsers: ['last 2 versions'] }) 33 | ], 34 | plugins: [ 35 | function() { this.plugin('done', writeStats) } 36 | ], 37 | resolve: { 38 | extensions: ['', '.js', '.json', '.jsx', '.es6', '.babel'], 39 | modulesDirectories: ['node_modules', 'app'] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webpack/dev-server.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import webpack from 'webpack'; 3 | import express from 'express'; 4 | 5 | import config from './dev.config'; 6 | 7 | const app = express(); 8 | const compiler = webpack(config.webpack); 9 | 10 | debug.enable('dev'); 11 | 12 | app.use(require('webpack-dev-middleware')(compiler, config.server.options)); 13 | app.use(require('webpack-hot-middleware')(compiler)); 14 | 15 | app.listen(config.server.port, '0.0.0.0', function() { 16 | debug('dev')('`webpack-dev-server` listening on port %s', config.server.port); 17 | }); 18 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { isArray } from 'lodash'; 4 | 5 | import writeStats from './utils/write-stats'; 6 | import startExpress from './utils/start-express'; 7 | import { baseConfig } from './base.config'; 8 | 9 | const PORT = parseInt(process.env.PORT, 10) + 1 || 3001; 10 | const LOCAL_IP = require('dev-ip')(); 11 | const HOST = isArray(LOCAL_IP) && LOCAL_IP[0] || LOCAL_IP || 'localhost'; 12 | const PUBLIC_PATH = `http://${HOST}:${PORT}/assets/`; 13 | 14 | export default { 15 | server: { 16 | port: PORT, 17 | options: { 18 | publicPath: PUBLIC_PATH, 19 | hot: true, 20 | stats: { 21 | assets: true, 22 | colors: true, 23 | version: false, 24 | hash: false, 25 | timings: true, 26 | chunks: false, 27 | chunksModule: false 28 | } 29 | } 30 | }, 31 | webpack: { 32 | ...baseConfig, 33 | devtool: 'cheap-module-source-map', 34 | entry: { 35 | app: [ 36 | `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/__webpack_hmr`, 37 | './app/index.js' 38 | ] 39 | }, 40 | output: { 41 | ...baseConfig.output, 42 | publicPath: PUBLIC_PATH 43 | }, 44 | module: { 45 | ...baseConfig.module, 46 | loaders: [ 47 | ...baseConfig.module.loaders, 48 | { 49 | test: /\.(jpe?g|png|gif|svg|woff|woff2|eot|ttf)(\?v=[0-9].[0-9].[0-9])?$/, 50 | loader: 'file?name=[sha512:hash:base64:7].[ext]', 51 | exclude: /node_modules\/(?!font-awesome|bootstrap)/ 52 | }, 53 | { 54 | test: /\.css$/, 55 | loader: 'style!css!postcss', 56 | exclude: /node_modules\/(?!font-awesome|bootstrap)/ 57 | } 58 | ] 59 | }, 60 | plugins: [ 61 | new webpack.HotModuleReplacementPlugin(), 62 | new webpack.NoErrorsPlugin(), 63 | 64 | new webpack.DefinePlugin({ 65 | 'process.env': { 66 | BROWSER: JSON.stringify('true'), 67 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') 68 | } 69 | }), 70 | 71 | new webpack.optimize.DedupePlugin(), 72 | new webpack.optimize.OccurenceOrderPlugin(), 73 | 74 | function() { this.plugin('done', writeStats); }, 75 | function() { this.plugin('done', startExpress); } 76 | ] 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 4 | 5 | import { baseConfig } from './base.config'; 6 | 7 | export default { 8 | ...baseConfig, 9 | module: { 10 | ...baseConfig.module, 11 | loaders: [ 12 | ...baseConfig.module.loaders, 13 | { 14 | test: /\.(svg|woff|woff2|eot|ttf)(\?v=[0-9].[0-9].[0-9])?$/, 15 | loader: 'file?name=[sha512:hash:base64:7].[ext]', 16 | exclude: /node_modules\/(?!font-awesome|bootstrap)/ 17 | }, 18 | { 19 | test: /\.(jpe?g|png|gif)$/, 20 | loader: 'file?name=[sha512:hash:base64:7].[ext]!image?optimizationLevel=7&progressive&interlaced', 21 | exclude: /node_modules\/(?!font-awesome|bootstrap)/ 22 | }, 23 | { 24 | test: /\.css$/, 25 | loader: ExtractTextPlugin.extract('style', 'css!postcss') 26 | } 27 | ] 28 | }, 29 | plugins: [ 30 | new ExtractTextPlugin('[name]-[hash].css'), 31 | 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | BROWSER: JSON.stringify(true), 35 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'production') 36 | } 37 | }), 38 | 39 | // optimizations 40 | new webpack.optimize.DedupePlugin(), 41 | new webpack.optimize.OccurenceOrderPlugin(), 42 | new webpack.optimize.UglifyJsPlugin({ 43 | compress: { 44 | warnings: false, 45 | screw_ie8: true, 46 | sequences: true, 47 | dead_code: true, 48 | drop_debugger: true, 49 | comparisons: true, 50 | conditionals: true, 51 | evaluate: true, 52 | booleans: true, 53 | loops: true, 54 | unused: true, 55 | hoist_funs: true, 56 | if_return: true, 57 | join_vars: true, 58 | cascade: true, 59 | drop_console: true 60 | }, 61 | output: { 62 | comments: false 63 | } 64 | }), 65 | 66 | ...baseConfig.plugins 67 | ] 68 | }; 69 | -------------------------------------------------------------------------------- /webpack/utils/start-express.js: -------------------------------------------------------------------------------- 1 | import cp from 'child_process'; 2 | import path from 'path'; 3 | 4 | import debug from 'debug'; 5 | import watch from 'node-watch'; 6 | import browserSync from 'browser-sync'; 7 | import { noop } from 'lodash'; 8 | 9 | let started; 10 | let server; 11 | let serverReload; 12 | 13 | const PORT = parseInt(process.env.PORT, 10) + 2 || 3002; 14 | const HOST = `0.0.0.0:${parseInt(process.env.PORT, 10) || 3000}`; 15 | const SERVER = path.resolve(__dirname, '../../server/index'); 16 | 17 | function startServer() { 18 | function restartServer() { 19 | debug('dev')('restarting express server'); 20 | serverReload = true; 21 | server.kill('SIGTERM'); 22 | return startServer(); 23 | } 24 | 25 | const env = { ...process.env, NODE_ENV: 'development', BABEL_ENV: 'server' }; 26 | server = cp.fork(SERVER, { env }); 27 | 28 | server.once('message', function(message) { 29 | if (message.match(/^online$/)) { 30 | // server restarted, reload page 31 | if (serverReload) { 32 | serverReload = false; 33 | browserSync.reload(); 34 | } 35 | 36 | if (!started) { 37 | started = true; 38 | browserSync({ port: PORT, proxy: HOST }); 39 | 40 | // Listen for `rs` in stdin to restart server 41 | debug('dev')('type `rs` in console to restart express server'); 42 | process.stdin.setEncoding('utf8'); 43 | process.stdin.on('data', function(data) { 44 | const parsedData = (data + '').trim().toLowerCase(); 45 | if (parsedData === 'rs') return restartServer(); 46 | }); 47 | 48 | // Start watch on server files 49 | // and reload browser on changes 50 | watch( 51 | path.resolve(__dirname, '../../server'), 52 | file => !file.match('webpack-stats.json') && restartServer() 53 | ); 54 | } 55 | } 56 | }); 57 | } 58 | 59 | process.on('exit', () => server.kill('SIGTERM')); 60 | export default () => !server ? startServer() : noop(); 61 | -------------------------------------------------------------------------------- /webpack/utils/write-stats.js: -------------------------------------------------------------------------------- 1 | // borrowed from https://github.com/gpbl/isomorphic500/blob/master/webpack%2Futils%2Fwrite-stats.js 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import debug from 'debug'; 5 | 6 | const filepath = path.resolve(__dirname, '../../server/webpack-stats.json'); 7 | 8 | export default function(stats) { 9 | const publicPath = this.options.output.publicPath; 10 | const json = stats.toJson(); 11 | 12 | // get chunks by name and extensions 13 | const getChunks = function(name, ext = /.js$/) { 14 | let chunks = json.assetsByChunkName[name]; 15 | 16 | // a chunk could be a string or an array, so make sure it is an array 17 | if (!(Array.isArray(chunks))) chunks = [chunks]; 18 | 19 | return chunks 20 | .filter(chunk => ext.test(path.extname(chunk))) // filter by extension 21 | .map(chunk => `${publicPath}${chunk}`); // add public path to it 22 | }; 23 | 24 | const script = getChunks('app', /js/); 25 | const style = getChunks('app', /css/); 26 | 27 | // Find compiled images in modules 28 | // it will be used to map original filename to the compiled one 29 | // for server side rendering 30 | const imagesRegex = /\.(jpe?g|png|gif|svg)$/; 31 | const images = json.modules 32 | .filter(module => imagesRegex.test(module.name)) 33 | .map(image => { 34 | return { 35 | original: image.name, 36 | compiled: `${publicPath}${image.assets[0]}` 37 | }; 38 | }); 39 | 40 | const content = { script, style, images }; 41 | 42 | fs.writeFileSync(filepath, JSON.stringify(content)); 43 | debug('dev')('`webpack-stats.json` updated'); 44 | } 45 | --------------------------------------------------------------------------------