├── .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 |
43 | { collection.map(({ name, picture, seed }) =>
44 | -
45 |
46 |
49 | { name.first } { name.last }
50 |
51 |
52 | ) }
53 |
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 |
--------------------------------------------------------------------------------