├── .babelrc
├── .coveralls.yml
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .nvmrc
├── .travis.yml
├── Dockerfile
├── Procfile
├── README.md
├── app
├── actions
│ ├── locale.js
│ ├── notification.js
│ ├── notifications.js
│ ├── page-title.js
│ ├── requests.js
│ └── users.js
├── components
│ ├── app.jsx
│ ├── footer.jsx
│ ├── guides.jsx
│ ├── header.jsx
│ ├── notifications-kitchen.jsx
│ ├── notifications.jsx
│ ├── profile.jsx
│ ├── protected.jsx
│ ├── shared
│ │ ├── lang-picker.jsx
│ │ ├── require-auth.jsx
│ │ └── spinner.jsx
│ └── users.jsx
├── data
│ ├── en.js
│ ├── fr.js
│ └── users.json
├── fonts
│ ├── Roboto-Medium-webfont.eot
│ ├── Roboto-Medium-webfont.svg
│ ├── Roboto-Medium-webfont.ttf
│ └── Roboto-Medium-webfont.woff
├── images
│ ├── favicon.ico
│ ├── react-logo.png
│ └── spinner.svg
├── index.js
├── main.js
├── pages
│ ├── login-info.jsx
│ ├── not-found.jsx
│ └── server-error.jsx
├── routes.jsx
├── stores
│ ├── locale.js
│ ├── notification.js
│ ├── notifications.js
│ ├── page-title.js
│ ├── requests.js
│ └── users.js
├── styles
│ ├── _fonts.css
│ ├── _variables.css
│ ├── app.css
│ ├── footer.css
│ ├── header.css
│ ├── lang-picker.css
│ ├── spinner.css
│ └── users.css
└── utils
│ ├── alt-resolver.js
│ ├── flux.js
│ ├── image-resolver.js
│ ├── intl-loader.js
│ ├── intl-polyfill.js
│ └── localized-routes.jsx
├── docker-compose.yml
├── karma.conf.js
├── logs
└── .gitkeep
├── main-heroku.js
├── package.json
├── processes.json
├── server
├── config
│ ├── all.json
│ ├── development.js
│ ├── init.js
│ └── production.js
├── index.js
├── koa.js
├── router.jsx
└── views
│ ├── layouts
│ └── index.hbs
│ └── main.hbs
├── test
├── spec
│ ├── components
│ │ ├── app.test.jsx
│ │ ├── header.test.jsx
│ │ ├── lang-picker.test.jsx
│ │ ├── profile.test.jsx
│ │ └── users.test.jsx
│ ├── pages
│ │ ├── not-found.test.jsx
│ │ └── server-error.test.jsx
│ ├── stores
│ │ └── users.test.js
│ └── utils
│ │ ├── alt-resolver.test.js
│ │ ├── image-resolver.test.js
│ │ ├── intl-loader.test.js
│ │ └── localized-routes.test.js
└── utils
│ ├── inject-lang.js
│ └── stub-router-context.jsx
├── tests.webpack.js
└── webpack
├── base.config.js
├── dev-server.js
├── dev.config.js
├── prod.config.js
└── utils
├── clean-dist.js
├── start-koa.js
└── write-stats.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "env": {
4 | "browser": {
5 | "plugins": ["react-transform"],
6 | "extra": {
7 | "react-transform": [
8 | {
9 | "target": "react-transform-hmr",
10 | "imports": ["react"],
11 | "locals": ["module"]
12 | },
13 | {
14 | "target": "react-transform-catch-errors",
15 | "imports": ["react", "redbox-react"]
16 | }
17 | ]
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: 3eAArXSaxEd4JCV4030oyx4Ex7QhS5M3e
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | .coveralls.yml
4 | .editorconfig
5 | .gitattributes
6 | .gitignore
7 | .git
8 | .travis.yml
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | node_modules/
3 | dist/
4 | test/
5 |
--------------------------------------------------------------------------------
/.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 | "mocha": true
10 | },
11 | "globals": {
12 | "chai": true
13 | },
14 | "rules": {
15 | "react/display-name": [2, { "acceptTranspilerName": true }],
16 | "react/jsx-curly-spacing": [2, "always"],
17 | "react/jsx-no-duplicate-props": 2,
18 | "react/jsx-no-undef": 2,
19 | "react/jsx-quotes": 0,
20 | "react/jsx-uses-react": 2,
21 | "react/jsx-uses-vars": 2,
22 | "react/no-did-mount-set-state": 2,
23 | "react/no-did-update-set-state": 2,
24 | "react/no-multi-comp": 2,
25 | "react/no-unknown-property": 2,
26 | "react/prop-types": 2,
27 | "react/react-in-jsx-scope": 2,
28 | "react/require-extension": 2,
29 | "react/self-closing-comp": 2,
30 | "react/wrap-multilines": 2,
31 | "react/sort-comp": 0,
32 |
33 | "quotes": [2, "single", "avoid-escape"],
34 | "jsx-quotes": [2, "prefer-single"],
35 | "comma-dangle": [2, "never"],
36 | "indent": [2, 2],
37 | "object-curly-spacing": [2, "always"],
38 | "no-undef": 2,
39 | "no-underscore-dangle": 0,
40 | "func-names": 0,
41 | "no-else-return": 0,
42 | "no-console": 0,
43 | "no-throw-literal": 0,
44 | "id-length": 0
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | text eol=lf
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/webpack-stats.json
2 | coverage
3 | node_modules
4 | npm-debug.log
5 | dist
6 | .tmp
7 | .DS_Store
8 | .sass-cache
9 | .env
10 | .c9
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 4.0.0
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.0.0"
4 | before_install:
5 | - "export DISPLAY=:99.0"
6 | - "sh -e /etc/init.d/xvfb start"
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mhart/alpine-node
2 | EXPOSE 3000
3 | EXPOSE 3001
4 | EXPOSE 3002
5 | EXPOSE 3003
6 |
7 | WORKDIR /src
8 |
9 | RUN apk add --update \
10 | build-base \
11 | autoconf \
12 | automake \
13 | file \
14 | nasm \
15 | libpng-dev \
16 | python \
17 | bash \
18 | && rm -rf /var/cache/apk/*
19 |
20 | # Install and cache node_modules
21 | ADD package.json /src/package.json
22 | RUN npm install
23 |
24 | # We need to add `.babelrc` as same level as `node_modules`
25 | ADD .babelrc /src/.babelrc
26 |
27 | # Add `node_modules/.bin` to $PATH
28 | ENV PATH /src/node_modules/.bin:$PATH
29 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node main-heroku.js
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/iam4x/isomorphic-flux-boilerplate)
2 | [](https://coveralls.io/github/iam4x/isomorphic-flux-boilerplate?branch=master)
3 | [](https://david-dm.org/iam4x/isomorphic-flux-boilerplate)
4 | [](https://david-dm.org/iam4x/isomorphic-flux-boilerplate#info=devDependencies)
5 | [](https://www.npmjs.com/package/isomorphic-flux-boilerplate)
6 |
7 | # ES6 Isomorphic Flux/ReactJS Boilerplate
8 |
9 | > A wonderfull boilerplate for **Flux/ReactJS** [isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) applications, running on **Koa**.
10 |
11 | **Demo:** http://isomorphic.iam4x.fr
12 |
13 | ## Libraries Included
14 |
15 | * [react](https://facebook.github.io/react/)
16 | * [react-router](https://github.com/rackt/react-router)
17 | * [react-intl](https://github.com/yahoo/react-intl)
18 | * [react-redbox](https://github.com/KeywordBrain/redbox-react)
19 | * [alt](https://github.com/goatslacker/alt)
20 | * [iso](https://github.com/goatslacker/iso)
21 | * [koa](http://koajs.com/)
22 | * [webpack](http://webpack.github.io/)
23 | * [webpack-hot-middleware](https://github.com/glenjamin/webpack-hot-middleware)
24 | * [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware)
25 | * [babeljs](https://babeljs.io/)
26 | * [cssnext](http://cssnext.io/)
27 |
28 | ## TL;DR
29 |
30 | Use with `nodejs^4.x`, clone the repo, `npm install` and `npm run dev`.
31 |
32 | Learn React ([react-prime-draft](https://github.com/mikechau/react-primer-draft)), learn Flux and Alt ([alt guide](http://alt.js.org/guide/)).
33 |
34 | Wrap you async actions into promises, send them to `altResolver` with `altResolver.resolve(xxx)` for async server side rendering (see [app/actions/users.js:31](https://github.com/iam4x/isomorphic-flux-boilerplate/blob/master/app/actions/users.js#L31)).
35 |
36 | Build for production with `npm run build`, don't forget to run the tests before `npm test`.
37 |
38 | ## Concepts
39 |
40 | **Koa** will be our server for the server side rendering, we use **alt** for our Flux architecture and **react-router** for routing in our app.
41 |
42 | With **iso** as helper we can populate **alt** flux stores before the first rendering and have a complete async isomorphic React application.
43 |
44 | Run this boilerplate, you will see the server is fetching some fake users and will populate the `UserStore` with this data. **Koa** will render the first markup, serve the JavaScript and then it will entirely run on the client.
45 |
46 | ## Flux
47 |
48 | We use [alt](http://alt.js.org) instance as [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html) implementation.
49 |
50 | We need to use instances for isomorphic applications, to have a unique store/actions per requests on the server.
51 |
52 | On the client, Flux is initialized in `app/main.js` and sent to our first React Component via props (`this.props.flux`). Everytime you want to uses stores or actions in a component you need to give it access through props.
53 |
54 | On the server, it's similar but Flux is initialized in `server/router.jsx`. The instance is sent to `alt-resolver` for rendering components with the correct props.
55 |
56 | Learn more about [alt instances](http://alt.js.org/docs/altInstances) in the alt documentation.
57 |
58 | ## Internationalization (i18n)
59 |
60 | We use [react-intl](https://github.com/yahoo/react-intl) for internationalization, it uses browser implementation of [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). For older browser and for node, we load the polyfill.
61 |
62 | * Support localized strings (see [data/en.js](https://github.com/iam4x/isomorphic-flux-boilerplate/blob/master/app%2Fdata%2Fen.js))
63 | * Support localized dates, times and currencies.
64 |
65 | Lang files and Intl polyfill are compiled into webpack chunks, for lazy-loading depending the locale of the user.
66 |
67 | If user changes locale, it is saved into a cookie `_lang` and used by the server to know the locale of rendering. If there's no `_lang` cookie, server will rely on `Accept-Language` request header. Server will set `` on rendering.
68 |
69 | Thank's to [gpbl/react-locale-hot-switch](https://github.com/gpbl/react-locale-hot-switch) for the implementation example!
70 |
71 | In order to use `FormattedRelative` you have to pass `{...this.props}` to component:
72 |
73 | ```
74 |
77 | ```
78 |
79 | ## Localized routes
80 |
81 | We have an utility to generate severals routes for the same component (see `app/utils/localized-routes.js`).
82 |
83 | Use the same logic as localized string, declare the localized routes into `app/routes.js` and into your `data/{lang}` file.
84 |
85 | ## Async data-fetching
86 |
87 | Alt-resolver is the magic thing about the boilerplate, it will be our tool for resolving promises (data-fetching) before server side rendering.
88 |
89 | Wrap data-fetching requests from actions into promises and send them to `altResolver` like:
90 |
91 | ```
92 | fetch() {
93 | const promise = (resolve) => {
94 | request
95 | .get('http://example.com/api/users')
96 | .end((response) => {
97 | // fire new action to send data to store
98 | this.actions.fetchSuccess(response.body);
99 | return resolve();
100 | });
101 | };
102 | // Send the `promise` to altResolver
103 | this.alt.resolve(promise);
104 | }
105 | ```
106 |
107 | Call the fetch action from component in the `componentWillMount` method:
108 |
109 | ```
110 | static propTypes: {
111 | flux: React.PropTypes.object.isRequired
112 | }
113 |
114 | componentWillMount() {
115 | const usersActions = this.props.flux.getActions('users');
116 | return usersActions.fetch();
117 | }
118 | ```
119 |
120 | On browser side, the rendering won't be stopped and will resolve the promise instantly.
121 |
122 | On server side, `altResolver.render` will fire a first render to collect all the promises needed for a complete rendering. It will then resolve them, and try to re-render the application for a complete markup.
123 |
124 | Open `app/actions/users.js`, `app/utils/alt-resolver.js`, `app/stores/users.js` for more information about data-fetching.
125 |
126 | ## How to `require()` images on server side
127 |
128 | On client with webpack, you can directly `require()` images for your images DOM element like:
129 |
130 | ```
131 |
132 | ```
133 |
134 | Webpack will load them through the `url-loader` and if it's too big it will sent through `file-loader` for minification/compilation. The results is an image with a new filename for cache busting.
135 |
136 | But on node, `require()` an image will just throw an exception. There's an util for loading image on server side to achieve this:
137 |
138 | ```
139 | import imageResolver from 'utils/image-resolver'
140 |
141 | let image;
142 | // On browser just require() the image as usual
143 | if (process.env.BROWSER) {
144 | image = require('images/logo.png');
145 | }
146 | else {
147 | image = imageResolver('images/logo.png');
148 | }
149 |
150 | ...
151 | render () {
152 | return (
153 |
154 | );
155 | }
156 | ...
157 | ```
158 |
159 | The utils/image-resolver with match the original image name with the compiled one.
160 |
161 | Voilà! You can `require()` images on server side too.
162 |
163 | ## Installation / How-to
164 |
165 | It's super easy to do with [nvm](https://github.com/creationix/nvm):
166 |
167 | * `$ nvm install stable`
168 | * `$ nvm use stable`
169 | * `$ nvm alias default stable`
170 |
171 | After that, you will just need to clone the repo and install dependancies:
172 |
173 | * `$ git clone -o upstream https://github.com/iam4x/isomorphic-flux-boilerplate.git app`
174 | * `$ cd app && npm install`
175 |
176 | (Don't forget to add your remote origin: `$ git remote origin git@github.com:xxx/xxx.git`)
177 |
178 | ### Run the project in development:
179 |
180 | * `$ npm run dev`
181 |
182 | Open your browser to `http://localhost:3002` and you will see the magic happens! Try to disable JavaScript in your browser, you will still be able to navigate between pages of the application. Enjoy the power of isomorphic applications!
183 |
184 | (Note: ports 3000-3002 are needed, you can change this with `$ PORT=3050 npm run dev` it will run on 3050-3052)
185 |
186 | ### Run tests
187 |
188 | * `$ npm test` will run the tests once
189 | * `$ npm run dev-test` will watch for changes and run the tests on change
190 |
191 | ### Build project:
192 |
193 | Just run `$ npm run build`, it will produce these tasks:
194 |
195 | * Run tests from `test/spec/**/*.jsx`
196 | * Concat & minify styles to `/dist/app-[hash].css`
197 | * Concat & minify scripts to `/dist/js/app-[hash].js`
198 |
199 | ### Update the boilerplate
200 |
201 | You can fetch the upstream branch and merge it into your master:
202 |
203 | * `$ git checkout master`
204 | * `$ git fetch upstream`
205 | * `$ git merge upstream/master`
206 | * `$ npm install`
207 |
208 | ### Run in production
209 |
210 | Build the project first:
211 |
212 | * `$ npm run build`
213 |
214 | Then start the koa server:
215 |
216 | * `$ NODE_ENV=production node server/index.js`
217 |
218 | You can also use `processes.json` to run the application with [PM2 Monitor](https://github.com/Unitech/pm2) on your production server (customize it for your use):
219 |
220 | * `$ pm2 start processes.json`
221 |
222 | ### (OSX) Run into docker for development
223 |
224 | You can build and dev with the boilerplate through docker container, it runs with dinghy.
225 |
226 | * Install [dinghy](https://github.com/codekitchen/dinghy) (it has support for NFS sharing which is required for changes detection and it's fast!)
227 | * `$ docker-compose up && open http://webapp.docker`
228 |
229 | ### Learn more
230 |
231 | * [Official ReactJS website](http://facebook.github.io/react/)
232 | * [Official ReactJS wiki](https://github.com/facebook/react/wiki)
233 | * [Official Flux website](http://facebook.github.io/flux/)
234 | * [ReactJS Conf 2015 links](https://gist.github.com/yannickcr/148110d3ca658ad96c2b)
235 | * [Learn ES6](https://babeljs.io/docs/learn-es6/)
236 | * [ES6 Features](https://github.com/lukehoban/es6features#readme)
237 |
--------------------------------------------------------------------------------
/app/actions/locale.js:
--------------------------------------------------------------------------------
1 | import intlLoader from 'utils/intl-loader';
2 |
3 | class LocaleActions {
4 | constructor() {
5 | this.generateActions('switchLocaleSuccess');
6 | }
7 |
8 | async switchLocale(locale) {
9 | if (locale) {
10 | const { messages } = await intlLoader(locale);
11 | return this.actions.switchLocaleSuccess({ locale, messages });
12 | }
13 | }
14 | }
15 |
16 | export default LocaleActions;
17 |
--------------------------------------------------------------------------------
/app/actions/notification.js:
--------------------------------------------------------------------------------
1 | class NotificationActions {
2 |
3 | constructor() {
4 | this.generateActions('add', 'remove', 'success', 'error', 'warning', 'info');
5 | }
6 | }
7 |
8 | export default NotificationActions;
9 |
--------------------------------------------------------------------------------
/app/actions/notifications.js:
--------------------------------------------------------------------------------
1 | class NotificationsActions {
2 |
3 | constructor() {
4 | this.generateActions('successfullyAdded', 'successfullyRemoved');
5 | }
6 | }
7 |
8 | export default NotificationsActions;
9 |
--------------------------------------------------------------------------------
/app/actions/page-title.js:
--------------------------------------------------------------------------------
1 | class PageTitleActions {
2 |
3 | constructor() {
4 | this.generateActions('set');
5 | }
6 |
7 | }
8 |
9 | export default PageTitleActions;
10 |
--------------------------------------------------------------------------------
/app/actions/requests.js:
--------------------------------------------------------------------------------
1 | class RequestsActions {
2 |
3 | constructor() {
4 | this.generateActions('start', 'success', 'fail');
5 | }
6 |
7 | }
8 |
9 | export default RequestsActions;
10 |
--------------------------------------------------------------------------------
/app/actions/users.js:
--------------------------------------------------------------------------------
1 | import sample from 'lodash/collection/sample';
2 | import take from 'lodash/array/take';
3 |
4 | import data from 'data/users.json';
5 |
6 | class UsersActions {
7 |
8 | constructor() {
9 | this.generateActions(
10 | 'remove', 'fetchSuccess', 'fetchBySeedSuccess'
11 | );
12 | }
13 |
14 | add() {
15 | const promise = (resolve) => {
16 | // fake xhr
17 | this.alt.getActions('requests').start();
18 | setTimeout(() => {
19 | // Randomize this xhr
20 | const user = sample(data.users);
21 |
22 | if (Math.random() > 0.5) {
23 | this.actions.addSuccess(user);
24 | this.alt.getActions('requests').success();
25 | return resolve();
26 | } else {
27 | this.alt.getActions('requests').fail();
28 | this.actions.addError(user);
29 | return resolve();
30 | }
31 | }, 300);
32 | };
33 | this.alt.resolve(promise);
34 | }
35 |
36 | addSuccess(user) {
37 | this.alt.getActions('notification').success({
38 | title: 'User added',
39 | message: `${user.user.email} was added!`
40 | });
41 |
42 | return { user };
43 | }
44 |
45 | addError(user) {
46 | this.alt.getActions('notification').error({
47 | title: 'Error adding user',
48 | message: `${user.user.email} couldn't be added.`
49 | });
50 | }
51 |
52 | fetch() {
53 | const promise = (resolve) => {
54 | this.alt.getActions('requests').start();
55 | setTimeout(() => {
56 | this.actions.fetchSuccess(take(data.users, 10));
57 | this.alt.getActions('requests').success();
58 | return resolve();
59 | }, 300);
60 | };
61 | this.alt.resolve(promise);
62 | }
63 |
64 | fetchBySeed(seed) {
65 | const promise = (resolve) => {
66 | this.alt.getActions('requests').start();
67 | setTimeout(() => {
68 | const user = data.users.find((u) => u.seed === seed);
69 | this.actions.fetchBySeedSuccess(user);
70 | this.alt.getActions('requests').success();
71 | return resolve();
72 | }, 300);
73 | };
74 |
75 | this.alt.resolve(promise);
76 | }
77 |
78 | }
79 |
80 | export default UsersActions;
81 |
--------------------------------------------------------------------------------
/app/components/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import Header from 'components/header';
4 | import Footer from 'components/footer';
5 | import Notifications from 'components/notifications';
6 |
7 | if (process.env.BROWSER) require('styles/app.css');
8 |
9 | class App extends Component {
10 |
11 | static propTypes = {
12 | flux: PropTypes.object.isRequired,
13 | children: PropTypes.element
14 | }
15 |
16 | constructor(props, context) {
17 | super(props, context);
18 | this.state = { i18n: props.flux.getStore('locale').getState() };
19 | }
20 |
21 | componentDidMount() {
22 | const { flux } = this.props;
23 | flux.getStore('locale').listen(this._handleLocaleChange);
24 | flux.getStore('page-title').listen(this._handlePageTitleChange);
25 | }
26 |
27 | componentWillUnmount() {
28 | const { flux } = this.props;
29 | flux.getStore('locale').unlisten(this._handleLocaleChange);
30 | flux.getStore('page-title').unlisten(this._handlePageTitleChange);
31 | }
32 |
33 | _handleLocaleChange = (i18n) => this.setState({ i18n })
34 | _handlePageTitleChange = ({ title }) => document.title = title
35 |
36 | // If we have children components sent by `react-router`
37 | // we need to clone them and add them the correct
38 | // locale and messages sent from the Locale Store
39 | renderChild = (child) =>
40 | React.cloneElement(child, { ...this.state.i18n });
41 |
42 | render() {
43 | return (
44 |
45 |
48 |
49 | { React.Children
50 | .map(this.props.children, this.renderChild) }
51 |
52 |
53 |
55 |
56 | );
57 | }
58 |
59 | }
60 |
61 | export default App;
62 |
--------------------------------------------------------------------------------
/app/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Footer extends Component {
4 |
5 | render() {
6 | return (
7 |
12 | );
13 | }
14 |
15 | }
16 |
17 | export default Footer;
18 |
--------------------------------------------------------------------------------
/app/components/guides.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { IntlMixin } from 'react-intl';
3 |
4 | class Guides extends Component {
5 |
6 | static propTypes = {
7 | flux: PropTypes.object.isRequired
8 | }
9 |
10 | _getIntlMessage = IntlMixin.getIntlMessage
11 |
12 | componentWillMount() {
13 | this.props.flux
14 | .getActions('page-title')
15 | .set(this._getIntlMessage('guides.page-title'));
16 | }
17 |
18 | render() {
19 | return (
20 |
21 |
Guides
22 |
Coming soon...
23 |
24 | );
25 | }
26 |
27 | }
28 |
29 | export default Guides;
30 |
--------------------------------------------------------------------------------
/app/components/header.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { IntlMixin } from 'react-intl';
4 |
5 | import imageResolver from 'utils/image-resolver';
6 | import Spinner from 'components/shared/spinner';
7 | import LangPicker from 'components/shared/lang-picker';
8 |
9 | // Load styles for the header
10 | // and load the `react-logo.png` image
11 | // for the `
` element
12 | let reactLogo;
13 | if (process.env.BROWSER) {
14 | reactLogo = require('images/react-logo.png');
15 | } else {
16 | reactLogo = imageResolver('images/react-logo.png');
17 | }
18 |
19 | class Header extends Component {
20 |
21 | static propTypes = {
22 | flux: PropTypes.object.isRequired,
23 | locales: PropTypes.array.isRequired
24 | }
25 |
26 | _getIntlMessage = IntlMixin.getIntlMessage
27 |
28 | state = {
29 | spinner: false
30 | }
31 |
32 | componentDidMount() {
33 | this.props.flux
34 | .getStore('requests')
35 | .listen(this._handleRequestStoreChange);
36 | }
37 |
38 | _handleRequestStoreChange = ({ inProgress }) =>
39 | this.setState({ spinner: inProgress })
40 |
41 | render() {
42 | const { locales, flux } = this.props;
43 | const [ activeLocale ] = locales;
44 |
45 | return (
46 |
47 | {/* Spinner in the top right corner */}
48 |
49 |
50 | {/* LangPicker on the right side */}
51 |
54 |
55 | {/* React Logo in header */}
56 |
57 |
58 |
59 |
60 | {/* Links in the navbar */}
61 |
62 | -
63 |
64 | { this._getIntlMessage('header.users') }
65 |
66 |
67 | -
68 |
69 | { this._getIntlMessage('header.guides') }
70 |
71 |
72 | -
73 |
74 | { this._getIntlMessage('header.protected') }
75 |
76 |
77 | -
78 |
79 | { this._getIntlMessage('header.notifications') }
80 |
81 |
82 |
83 |
84 | );
85 | }
86 | }
87 |
88 | export default Header;
89 |
--------------------------------------------------------------------------------
/app/components/notifications-kitchen.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { IntlMixin } from 'react-intl';
3 |
4 | class NotificationsKitchen extends Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | saveFileNotification: false,
10 | retryNotification: {
11 | firstTime: true,
12 | notification: false,
13 | text: 'Example text'
14 | }
15 | };
16 | }
17 |
18 | _getIntlMessage = IntlMixin.getIntlMessage
19 |
20 | static propTypes = {
21 | flux: PropTypes.object.isRequired
22 | }
23 |
24 | componentWillMount() {
25 | this.props.flux
26 | .getActions('page-title')
27 | .set(this._getIntlMessage('users.page-title'));
28 | }
29 |
30 | _saveFileStart() {
31 | const self = this;
32 | this.props.flux.getActions('notification').info({
33 | title: 'Saving file',
34 | message: 'Please wait until your file is saved...',
35 | position: 'tc',
36 | autoDismiss: 0,
37 | dismissible: false,
38 | onRemove: function() {
39 | self.setState({ saveFileNotification: false });
40 | },
41 | onAdd: function(notification) {
42 | self.setState({ saveFileNotification: notification });
43 | }
44 | });
45 |
46 | // Add a timeout to simulate the saving file finished
47 | setTimeout(function() {
48 | self._saveFileFinish();
49 | }, 5000);
50 | }
51 |
52 | _saveFileFinish() {
53 | this.props.flux.getActions('notification').remove(this.state.saveFileNotification);
54 | this.props.flux.getActions('notification').success({
55 | title: 'File saved succesfully',
56 | message: 'Your file was saved.',
57 | position: 'tc',
58 | autoDismiss: 0,
59 | action: {
60 | label: 'Open file',
61 | callback: function() {
62 | console.log('Opening file...');
63 | }
64 | }
65 | });
66 | }
67 |
68 | _handleRetryNotificationTextChange(event) {
69 | const state = this.state;
70 | state.retryNotification.text = event.target.value;
71 | this.setState(state);
72 | }
73 |
74 | _saveRetryNotification() {
75 | const self = this;
76 | this.props.flux.getActions('notification').info({
77 | title: 'Saving information',
78 | message: 'Please wait until we save: ' + this.state.retryNotification.text,
79 | autoDismiss: 0,
80 | dismissible: false,
81 | onRemove: function() {
82 | const state = self.state;
83 | state.retryNotification.notification = false;
84 | self.setState(state);
85 | },
86 | onAdd: function(notification) {
87 | const state = self.state;
88 | state.retryNotification.notification = notification;
89 | self.setState(state);
90 | }
91 | });
92 |
93 | // Add a timeout to simulate the saving file finished
94 | setTimeout(function() {
95 | self._saveRetryNotificationFinish();
96 | }, 3000);
97 | }
98 |
99 | _saveRetryNotificationFinish() {
100 | const self = this;
101 | const state = this.state;
102 | this.props.flux.getActions('notification').remove(this.state.retryNotification.notification);
103 | if (this.state.retryNotification.firstTime) {
104 | this.props.flux.getActions('notification').error({
105 | title: 'We couldn\'t save your information',
106 | autoDismiss: 0,
107 | action: {
108 | label: 'Try again',
109 | callback: function() {
110 | self._saveRetryNotification();
111 | }
112 | }
113 | });
114 | state.retryNotification.firstTime = false;
115 | this.setState(state);
116 | } else {
117 | this.props.flux.getActions('notification').success({
118 | title: 'Information saved successfully'
119 | });
120 | }
121 | }
122 |
123 | render() {
124 | let saveFileButtonLabel = 'Click to save a file save';
125 |
126 | if (this.state.saveFileNotification) {
127 | saveFileButtonLabel = 'Saving...';
128 | }
129 | return (
130 |
131 |
132 |
Save file example
133 |
134 |
135 |
136 |
Retry action example
137 |
138 |
139 |
140 |
141 | );
142 | }
143 | }
144 |
145 | export default NotificationsKitchen;
146 |
--------------------------------------------------------------------------------
/app/components/notifications.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import ReactNotificationSystem from 'react-notification-system';
3 |
4 | class Notifications extends Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | notification: null,
10 | notifications: null
11 | };
12 | }
13 |
14 | static propTypes = {
15 | flux: PropTypes.object.isRequired
16 | }
17 |
18 | componentDidMount() {
19 | const { flux } = this.props;
20 | flux.getStore('notification').listen(this._handleNotificationChange);
21 | flux.getStore('notifications').listen(this._handleNotificationsChange);
22 | }
23 |
24 | componentWillUnmount() {
25 | const { flux } = this.props;
26 | flux.getStore('notification').unlisten(this._handleNotificationChange);
27 | flux.getStore('notifications').unlisten(this._handleNotificationsChange);
28 | }
29 |
30 | _handleNotificationsChange = ({ notifications }) => {
31 | this.setState({ notifications });
32 | }
33 |
34 | _handleNotificationChange = ({ notification, intent }) => {
35 | if (intent === 'add') {
36 | this.refs.notifications.addNotification(notification);
37 | } else if (intent === 'remove') {
38 | this.refs.notifications.removeNotification(notification);
39 | }
40 | };
41 |
42 | _removeNotification(notification) {
43 | let _notification = notification;
44 | // Check if the notification has UID, if not, try to find inside the store
45 | if (!notification.uid) {
46 | const notifications = this.state.notifications.slice();
47 | notifications.forEach(function(item) {
48 | if (item.uid === notification) {
49 | _notification = item;
50 | }
51 | });
52 | }
53 |
54 | this.props.flux.getActions('notification').remove(_notification);
55 | }
56 |
57 | render() {
58 | return ;
59 | }
60 | }
61 |
62 | export default Notifications;
63 |
--------------------------------------------------------------------------------
/app/components/profile.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { IntlMixin } from 'react-intl';
3 | import capitalize from 'lodash/string/capitalize';
4 |
5 | class Profile extends Component {
6 |
7 | static propTypes = {
8 | flux: PropTypes.object.isRequired,
9 | params: PropTypes.object.isRequired
10 | }
11 |
12 | _getIntlMessage = IntlMixin.getIntlMessage
13 | _formatMessage = IntlMixin.formatMessage.bind(Object.assign({}, this, IntlMixin))
14 |
15 | state = this.props.flux
16 | .getStore('users')
17 | .getBySeed(this.props.params.seed)
18 |
19 | componentWillMount() {
20 | this._setPageTitle();
21 |
22 | this.props.flux
23 | .getActions('users')
24 | .fetchBySeed(this.props.params.seed);
25 | }
26 |
27 | componentDidMount() {
28 | this.props.flux
29 | .getStore('users')
30 | .listen(this._handleStoreChange);
31 | }
32 |
33 | componentWillUnmount() {
34 | this.props.flux
35 | .getStore('users')
36 | .unlisten(this._handleStoreChange);
37 | }
38 |
39 | _handleStoreChange = () => {
40 | const user = this.props.flux
41 | .getStore('users')
42 | .getBySeed(this.props.params.seed);
43 |
44 | this.setState(user);
45 | this._setPageTitle();
46 | }
47 |
48 | _setPageTitle = () => {
49 | let title;
50 | if (this.state.user) {
51 | const user = this.state.user.user;
52 | const fullName = this._getFullName(user.name);
53 |
54 | title = this._getIntlMessage('profile.page-title');
55 | title = this._formatMessage(title, { fullName });
56 | } else {
57 | title = this._getIntlMessage('profile.not-found-page-title');
58 | }
59 |
60 | // Set page title
61 | this.props.flux
62 | .getActions('page-title')
63 | .set.defer(title);
64 | }
65 |
66 | _getFullName({ first, last }) {
67 | return `${capitalize(first)} ${capitalize(last)}`;
68 | }
69 |
70 | render() {
71 | if (this.state.user) {
72 | const user = this.state.user.user;
73 | return (
74 |
75 |
{ this._getFullName(user.name) }
76 |

79 |
80 | );
81 | }
82 |
83 | return (
84 | User not found
85 | );
86 | }
87 |
88 | }
89 |
90 | export default Profile;
91 |
--------------------------------------------------------------------------------
/app/components/protected.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { IntlMixin } from 'react-intl';
3 | import requireAuth from 'components/shared/require-auth';
4 |
5 | const Protected = requireAuth(class Protected extends Component {
6 |
7 | static propTypes = {
8 | flux: PropTypes.object.isRequired
9 | }
10 |
11 | _getIntlMessage = IntlMixin.getIntlMessage
12 |
13 | componentWillMount() {
14 | this.props.flux
15 | .getActions('page-title')
16 | .set(this._getIntlMessage('protected.page-title'));
17 | }
18 |
19 | render() {
20 | return (
21 |
22 |
Protected
23 |
secret mesaage
24 |
25 | );
26 | }
27 |
28 | });
29 |
30 | export default Protected;
31 |
--------------------------------------------------------------------------------
/app/components/shared/lang-picker.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import cx from 'classnames';
3 |
4 | class LangPicker extends Component {
5 |
6 | static propTypes = {
7 | activeLocale: PropTypes.string.isRequired,
8 | onChange: PropTypes.func.isRequired
9 | }
10 |
11 | locales = ['fr', 'en']
12 |
13 | render() {
14 | const { onChange, activeLocale } = this.props;
15 |
16 | return (
17 |
28 | );
29 | }
30 | }
31 |
32 | export default LangPicker;
33 |
--------------------------------------------------------------------------------
/app/components/shared/require-auth.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default function requireAuth(ChildComponent) {
4 | class Authenticated extends Component {
5 |
6 | static onEnter(next, redirect) {
7 | // Assume user is never authenticated
8 | // TODO: link with some API for better example
9 | const isAuthenticated = false;
10 | if (!isAuthenticated) return redirect({}, '/login-info');
11 | }
12 |
13 | render() {
14 | return ;
15 | }
16 | }
17 |
18 | return Authenticated;
19 | }
20 |
--------------------------------------------------------------------------------
/app/components/shared/spinner.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import cx from 'classnames';
3 |
4 | class Spinner extends Component {
5 |
6 | static propTypes = {
7 | active: PropTypes.bool
8 | }
9 |
10 | render() {
11 | const { active } = this.props;
12 | return ;
13 | }
14 | }
15 |
16 | export default Spinner;
17 |
--------------------------------------------------------------------------------
/app/components/users.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { IntlMixin } from 'react-intl';
4 | import { replaceParams } from 'utils/localized-routes';
5 |
6 | class Users extends Component {
7 |
8 | static propTypes = {
9 | flux: PropTypes.object.isRequired
10 | }
11 |
12 | _getIntlMessage = IntlMixin.getIntlMessage
13 |
14 | state = this.props.flux
15 | .getStore('users')
16 | .getState()
17 |
18 | componentWillMount() {
19 | this.props.flux
20 | .getActions('page-title')
21 | .set(this._getIntlMessage('users.page-title'));
22 |
23 | this.props.flux
24 | .getActions('users')
25 | .fetch();
26 | }
27 |
28 | componentDidMount() {
29 | this.props.flux
30 | .getStore('users')
31 | .listen(this._handleStoreChange);
32 | }
33 |
34 | componentWillUnmount() {
35 | this.props.flux
36 | .getStore('users')
37 | .unlisten(this._handleStoreChange);
38 | }
39 |
40 | _handleStoreChange = (state) => {
41 | return this.setState(state);
42 | }
43 |
44 | _removeUser(user, index) {
45 | this.props.flux
46 | .getActions('users')
47 | .remove(index);
48 |
49 | this.props.flux
50 | .getActions('notification')
51 | .info({
52 | title: 'User removed',
53 | message: user.user.email + ' removed.'
54 | });
55 | }
56 |
57 | renderUser = (user, index) => {
58 | const profileRoute = replaceParams(
59 | this._getIntlMessage('routes.profile'),
60 | { seed: user.seed }
61 | );
62 | return (
63 |
64 | { user.user.email } |
65 |
66 | Profile
67 | |
68 |
69 |
74 | |
75 |
76 | );
77 | }
78 |
79 | render() {
80 | return (
81 |
82 |
83 | { this._getIntlMessage('users.title') }
84 |
85 |
86 |
87 |
88 |
89 | { this._getIntlMessage('users.email') }
90 | |
91 |
92 | { this._getIntlMessage('users.actions') }
93 | |
94 |
95 |
96 |
97 | {
98 | this.state.users
99 | .map(this.renderUser)
100 | }
101 |
102 |
103 |
104 |
109 |
110 |
111 | );
112 | }
113 |
114 | }
115 |
116 | export default Users;
117 |
--------------------------------------------------------------------------------
/app/data/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | messages: {
3 | header: {
4 | users: 'Users List',
5 | guides: 'Guides',
6 | protected: 'Protected',
7 | notifications: 'Notifications'
8 | },
9 | guides: {
10 | 'page-title': 'Guides'
11 | },
12 | protected: {
13 | 'page-title': 'Protected Page'
14 | },
15 | profile: {
16 | 'page-title': 'Profile - {fullName}',
17 | 'not-found-page-title': 'User profile not found'
18 | },
19 | users: {
20 | 'page-title': 'Users',
21 | title: 'Some random users',
22 | email: 'Email address',
23 | actions: 'Actions',
24 | add: 'Add random user',
25 | profile: 'Profile'
26 | },
27 | notifications: {
28 | 'page-title': 'Notifications'
29 | },
30 | routes: {
31 | users: '/users',
32 | guides: '/guides',
33 | protected: '/protected',
34 | profile: '/profile/:seed',
35 | 'login-info': '/login-info',
36 | notifications: '/notifications'
37 | }
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/app/data/fr.js:
--------------------------------------------------------------------------------
1 | export default {
2 | messages: {
3 | header: {
4 | users: 'Utilisateurs',
5 | guides: 'Guides',
6 | protected: 'Privé',
7 | notifications: 'Notifications'
8 | },
9 | guides: {
10 | 'page-title': 'Guides'
11 | },
12 | protected: {
13 | 'page-title': 'Page protégée'
14 | },
15 | profile: {
16 | 'page-title': 'Profil - {fullName}',
17 | 'not-found-page-title': 'User profile not found'
18 | },
19 | users: {
20 | 'page-title': 'Utilisateurs',
21 | title: 'Des utilisateurs au hasard',
22 | email: 'Adresse email',
23 | actions: 'Opérations',
24 | add: 'Ajouter un utilisateur',
25 | profile: 'Profil'
26 | },
27 | notifications: {
28 | 'page-title': 'Notifications'
29 | },
30 | routes: {
31 | users: '/utilisateurs',
32 | guides: '/guides',
33 | protected: '/protege',
34 | profile: '/profil/:seed',
35 | 'login-info': '/info-client',
36 | notifications: '/notifications'
37 | }
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/app/data/users.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 |
--------------------------------------------------------------------------------
/app/fonts/Roboto-Medium-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/igorprado/isomorphic-react-app/b049ed23849c0f5b52dff48a240b622ff2d4c5ba/app/fonts/Roboto-Medium-webfont.eot
--------------------------------------------------------------------------------
/app/fonts/Roboto-Medium-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/igorprado/isomorphic-react-app/b049ed23849c0f5b52dff48a240b622ff2d4c5ba/app/fonts/Roboto-Medium-webfont.ttf
--------------------------------------------------------------------------------
/app/fonts/Roboto-Medium-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/igorprado/isomorphic-react-app/b049ed23849c0f5b52dff48a240b622ff2d4c5ba/app/fonts/Roboto-Medium-webfont.woff
--------------------------------------------------------------------------------
/app/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/igorprado/isomorphic-react-app/b049ed23849c0f5b52dff48a240b622ff2d4c5ba/app/images/favicon.ico
--------------------------------------------------------------------------------
/app/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/igorprado/isomorphic-react-app/b049ed23849c0f5b52dff48a240b622ff2d4c5ba/app/images/react-logo.png
--------------------------------------------------------------------------------
/app/images/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
38 |
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | // Browser ES6 Polyfill
2 | require('babel/polyfill');
3 |
4 | // Start application
5 | require('./main');
6 |
--------------------------------------------------------------------------------
/app/main.js:
--------------------------------------------------------------------------------
1 | import Iso from 'iso';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import Router from 'react-router';
5 | import createBrowserHistory from 'history/lib/createBrowserHistory';
6 |
7 | // Paths are relative to `app` directory
8 | import Flux from 'utils/flux';
9 | import intlLoader from 'utils/intl-loader';
10 |
11 | if (process.env.NODE_ENV === 'development') {
12 | require('debug').enable('dev,koa');
13 | }
14 |
15 | const boostrap = () => {
16 | return new Promise((resolve) => {
17 | Iso.bootstrap((initialState, __, container) => {
18 | resolve({ initialState, __, container });
19 | });
20 | });
21 | };
22 |
23 | (async () => {
24 | // Init alt instance
25 | const flux = new Flux();
26 |
27 | // bootstrap application with data from server
28 | const boot = await boostrap();
29 | flux.bootstrap(boot.initialState);
30 |
31 | // load the intl-polyfill if needed
32 | // load the correct data/{lang}.json into app
33 | const locale = flux.getStore('locale').getLocale();
34 | const { messages } = await intlLoader(locale);
35 | flux.getActions('locale').switchLocaleSuccess({ locale, messages });
36 |
37 | // load routes after int-polyfill
38 | // routes.jsx imports components using the `window.Intl`
39 | // it should be defined before
40 | const routerProps = {
41 | routes: require('routes'),
42 | history: createBrowserHistory(),
43 | createElement: (component, props) => {
44 | // Take locale and messages from `locale` store
45 | // and pass them to every components rendered from `Router`
46 | const i18n = flux.getStore('locale').getState();
47 | return React.createElement(component, { ...props, ...i18n, flux });
48 | }
49 | };
50 |
51 | // Render `` in the same container as the SSR
52 | ReactDOM.render(
53 | React.createElement(Router, { ...routerProps }),
54 | boot.container
55 | );
56 |
57 | // Tell `alt-resolver` we have done the first render
58 | // next promises will be resolved
59 | flux._resolver._firstClientSideRender = false;
60 | })();
61 |
--------------------------------------------------------------------------------
/app/pages/login-info.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class LoginInfo extends Component {
4 |
5 | render() {
6 | return Please log-in
;
7 | }
8 |
9 | }
10 |
11 | export default LoginInfo;
12 |
--------------------------------------------------------------------------------
/app/pages/not-found.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class NotFound extends Component {
4 |
5 | render() {
6 | return 404
;
7 | }
8 |
9 | }
10 |
11 | export default NotFound;
12 |
--------------------------------------------------------------------------------
/app/pages/server-error.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class ServerError extends Component {
4 |
5 | render() {
6 | return 500
;
7 | }
8 |
9 | }
10 |
11 | export default ServerError;
12 |
--------------------------------------------------------------------------------
/app/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router';
3 | import { generateRoute } from 'utils/localized-routes';
4 |
5 | export default (
6 |
7 | { generateRoute({
8 | paths: ['/', '/users', '/utilisateurs'],
9 | component: require('./components/users')
10 | }) }
11 | { generateRoute({
12 | paths: ['/protected', '/protege'],
13 | component: require('./components/protected')
14 | }) }
15 | { generateRoute({
16 | paths: ['/guides'],
17 | component: require('./components/guides')
18 | }) }
19 | { generateRoute({
20 | paths: ['/profile/:seed', '/profil/:seed'],
21 | component: require('./components/profile')
22 | }) }
23 | { generateRoute({
24 | paths: ['/notifications'],
25 | component: require('./components/notifications-kitchen')
26 | }) }
27 | { generateRoute({
28 | paths: ['/login-info', '/info-client'],
29 | component: require('./pages/login-info')
30 | }) }
31 |
32 |
33 | );
34 |
--------------------------------------------------------------------------------
/app/stores/locale.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | class LocaleStore {
4 |
5 | constructor() {
6 | this.bindActions(this.alt.getActions('locale'));
7 | this.locales = [''];
8 | this.messages = {};
9 | }
10 |
11 | static getLocale() {
12 | return this.getState().locales[0];
13 | }
14 |
15 | onSwitchLocaleSuccess(data) {
16 | // Save locale into a cookie
17 | // that will be read from server on requests
18 | if (process.env.BROWSER) {
19 | const Cookies = require('cookies-js');
20 | Cookies.set('_lang', data.locale, { expires: Infinity });
21 | debug('dev')(`updated _lang cookie to ${data.locale}`);
22 | }
23 |
24 | return this.setState({
25 | messages: data.messages,
26 | locales: [data.locale]
27 | });
28 | }
29 |
30 | }
31 |
32 | export default LocaleStore;
33 |
--------------------------------------------------------------------------------
/app/stores/notification.js:
--------------------------------------------------------------------------------
1 | class NotificationStore {
2 | constructor() {
3 | this.bindActions(this.alt.getActions('notification'));
4 |
5 | this.state = {
6 | notification: null,
7 | intent: null
8 | };
9 | }
10 |
11 | onAdd(notification) {
12 | return this._add(notification);
13 | }
14 |
15 | onRemove(notification) {
16 | return this._remove(notification);
17 | }
18 |
19 | onSuccess(notification) {
20 | return this._add(notification, 'success');
21 | }
22 |
23 | onError(notification) {
24 | return this._add(notification, 'error');
25 | }
26 |
27 | onWarning(notification) {
28 | return this._add(notification, 'warning');
29 | }
30 |
31 | onInfo(notification) {
32 | return this._add(notification, 'info');
33 | }
34 |
35 | _add(notification, level) {
36 | if (!notification) return false;
37 | if (level) notification.level = level;
38 | notification.onAdd = this._onAddWrapper.bind(this, notification.onAdd);
39 | notification.onRemove = this._onRemoveWrapper.bind(this, notification.onRemove);
40 | return this.setState({ notification, intent: 'add' });
41 | }
42 |
43 | _onAddWrapper(originalCallback, notification) {
44 | if (typeof originalCallback === 'function') {
45 | originalCallback(notification);
46 | }
47 | this.alt.getActions('notifications').successfullyAdded.defer(notification);
48 | }
49 |
50 | _onRemoveWrapper(originalCallback, notification) {
51 | if (typeof originalCallback === 'function') {
52 | originalCallback(notification);
53 | }
54 | this.alt.getActions('notifications').successfullyRemoved.defer(notification);
55 | }
56 |
57 | _remove(notification) {
58 | return this.setState({ notification, intent: 'remove' });
59 | }
60 | }
61 |
62 | export default NotificationStore;
63 |
--------------------------------------------------------------------------------
/app/stores/notifications.js:
--------------------------------------------------------------------------------
1 | class NotificationsStore {
2 | constructor() {
3 | this.bindActions(this.alt.getActions('notifications'));
4 |
5 | this.state = {
6 | notifications: []
7 | };
8 | }
9 |
10 | onSuccessfullyAdded(notification) {
11 | const notifications = this.state.notifications.slice();
12 | notifications.push(notification);
13 | return this.setState({ notifications });
14 | }
15 |
16 | onSuccessfullyRemoved(notification) {
17 | const notifications = this.state.notifications.slice();
18 | notifications.splice(notifications.indexOf(notification), 1);
19 | return this.setState({ notifications: notifications });
20 | }
21 | }
22 |
23 | export default NotificationsStore;
24 |
--------------------------------------------------------------------------------
/app/stores/page-title.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | class PageTitleStore {
4 |
5 | constructor() {
6 | this.bindActions(this.alt.getActions('page-title'));
7 |
8 | this.baseTitle = 'ISO-ReactJS';
9 | this.delimeter = '|';
10 |
11 | // Defaut title
12 | this.title = `${this.baseTitle}`;
13 | }
14 |
15 | onSet(title) {
16 | debug('dev')(`update page title to '${title}'`);
17 | this.title = `${this.baseTitle} ${this.delimeter} ${title}`;
18 | }
19 |
20 | }
21 |
22 | export default PageTitleStore;
23 |
--------------------------------------------------------------------------------
/app/stores/requests.js:
--------------------------------------------------------------------------------
1 | class RequestsStore {
2 |
3 | constructor() {
4 | this.bindActions(this.alt.getActions('requests'));
5 | this.inProgress = false;
6 | }
7 |
8 | onStart() {
9 | this._setInProgress(true);
10 | }
11 |
12 | onSuccess() {
13 | this._setInProgress(false);
14 | }
15 |
16 | onFail() {
17 | this._setInProgress(false);
18 | }
19 |
20 | _setInProgress(inProgress) {
21 | return this.setState({ inProgress });
22 | }
23 |
24 | }
25 |
26 | export default RequestsStore;
27 |
--------------------------------------------------------------------------------
/app/stores/users.js:
--------------------------------------------------------------------------------
1 | import findIndex from 'lodash/array/findIndex';
2 | import isEmpty from 'lodash/array/findIndex';
3 |
4 | class UsersStore {
5 |
6 | constructor() {
7 | this.bindActions(this.alt.getActions('users'));
8 | this.users = [];
9 | }
10 |
11 | static getBySeed(seed) {
12 | const users = this.getState().users;
13 | return { user: users.find((user) => user.seed === seed) };
14 | }
15 |
16 | onRemove(index) {
17 | const users = this.users.slice();
18 | users.splice(index, 1);
19 |
20 | return this.setState({ users });
21 | }
22 |
23 | onAddSuccess(user) {
24 | const users = this.users.slice();
25 | users.push(user);
26 | return this.setState({ users });
27 | }
28 |
29 | onFetchSuccess(users) {
30 | if (isEmpty(this.users)) {
31 | // just apply the new users
32 | // this is called on every server rendering
33 | return this.setState({ users });
34 | }
35 |
36 | const merged = this.users.slice();
37 | users.forEach((user) => {
38 | // update the most recent data into store
39 | let match = merged.find((u) => u.seed === user.seed) || null;
40 | if (match) {
41 | match = user;
42 | } else {
43 | // push the new user
44 | merged.push(user);
45 | }
46 | });
47 |
48 | return this.setState({ users: merged });
49 | }
50 |
51 | onFetchBySeedSuccess(user) {
52 | const users = this.users.slice();
53 | const index = findIndex(users, { seed: user.seed });
54 | if (index > -1) {
55 | users[index] = user;
56 | } else {
57 | users.push(user);
58 | }
59 |
60 | return this.setState({ users });
61 | }
62 |
63 | }
64 |
65 | export default UsersStore;
66 |
--------------------------------------------------------------------------------
/app/styles/_fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: var(--font-0);
3 | src: var(--url-0);
4 | src: var(--url-1) format('embedded-opentype'), var(--url-2) format('woff'), var(--url-3) format('truetype'), var(--url-4) format('svg');
5 | font-weight: normal;
6 | font-style: normal;
7 | }
8 |
--------------------------------------------------------------------------------
/app/styles/_variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-0: 'Roboto';
3 | --url-0: url('../fonts/Roboto-Medium-webfont.eot');
4 | --url-1: url('../fonts/Roboto-Medium-webfont.eot?#iefix');
5 | --url-2: url('../fonts/Roboto-Medium-webfont.woff');
6 | --url-3: url('../fonts/Roboto-Medium-webfont.ttf');
7 | --url-4: url('../fonts/Roboto-Medium-webfont.svg#robotomedium');
8 |
9 | --black: #000000;
10 | --white: #FFFFFF;
11 | }
12 |
--------------------------------------------------------------------------------
/app/styles/app.css:
--------------------------------------------------------------------------------
1 | @import 'normalize.css/normalize';
2 |
3 | @import './_fonts';
4 | @import './_variables';
5 |
6 | @import './footer';
7 | @import './header';
8 | @import './lang-picker';
9 | @import './spinner';
10 | @import './users';
11 |
12 | body {
13 | font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
14 | }
15 |
16 | .reset-list {
17 | margin: 0;
18 | padding: 0;
19 | list-style-type: none;
20 | }
21 |
22 | .text-center {
23 | text-align: center;
24 | }
25 |
26 | .un-select {
27 | user-select: none;
28 | }
29 |
--------------------------------------------------------------------------------
/app/styles/footer.css:
--------------------------------------------------------------------------------
1 | .app--footer {
2 | margin-top: 50px;
3 | }
4 |
5 | .app--footer-content {
6 | padding-top: 10px;
7 | text-align: center;
8 | }
9 |
10 | .app--footer iframe:first-child {
11 | margin-right: 25px;
12 | }
13 |
--------------------------------------------------------------------------------
/app/styles/header.css:
--------------------------------------------------------------------------------
1 | .app--header {
2 | position: relative;
3 | }
4 |
5 | .app--header .app--logo {
6 | display: block;
7 | height: 200px;
8 | margin: 0 auto;
9 | width: 200px;
10 | }
11 |
12 | .app--header .app--logo img {
13 | width: 100%;
14 | }
15 |
16 | .app--header .app--navbar li {
17 | display: inline-block;
18 | margin-right: 10px;
19 | }
20 |
21 | .app--header .app--navbar li:last-child {
22 | margin-right: 0;
23 | }
24 |
--------------------------------------------------------------------------------
/app/styles/lang-picker.css:
--------------------------------------------------------------------------------
1 | .lang--picker {
2 | bottom: -15px;
3 | position: absolute;
4 | right: 9px;
5 | }
6 |
7 | .lang--picker li {
8 | display: inline-block;
9 | margin-right: 10px;
10 | }
11 |
12 | .lang--picker li:last-child {
13 | margin-right: 0;
14 | }
15 |
16 | .lang--picker a {
17 | cursor: pointer;
18 | opacity: .5;
19 | padding: 0 5px;
20 | text-transform: uppercase;
21 | }
22 |
23 | .lang--picker a.active {
24 | background-color: var(--black);
25 | color: var(--white);
26 | opacity: 1;
27 | }
28 |
--------------------------------------------------------------------------------
/app/styles/spinner.css:
--------------------------------------------------------------------------------
1 | .app--spinner {
2 | background-image: url('../images/spinner.svg');
3 | background-repeat: no-repeat;
4 | display: none;
5 | height: 44px;
6 | position: absolute;
7 | right: 10px;
8 | top: 10px;
9 | width: 44px;
10 | }
11 |
12 | .app--spinner.active {
13 | display: block;
14 | }
15 |
--------------------------------------------------------------------------------
/app/styles/users.css:
--------------------------------------------------------------------------------
1 | .app--users {
2 | margin: 0 auto;
3 | }
4 |
5 | .app--users td,
6 | .app--users th {
7 | padding: 5px 10px;
8 | }
9 |
10 | .app--users thead {
11 | border-bottom: 1px solid var(--black);
12 | }
13 |
--------------------------------------------------------------------------------
/app/utils/alt-resolver.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 | import noop from 'lodash/utility/noop';
3 |
4 | import React from 'react';
5 | import ReactDOM from 'react-dom/server';
6 | import Iso from 'iso';
7 |
8 | import ErrorPage from 'pages/server-error';
9 |
10 | class AltResolver {
11 |
12 | _toResolve = []
13 | _firstClientSideRender = !(process.env.NODE_ENV === 'test')
14 |
15 | resolve(promise: Function, later = false) {
16 | if (process.env.BROWSER && !later) {
17 | // Prevent first app mount to re-resolve same
18 | // promises that server already did
19 | if (this._firstClientSideRender) {
20 | return noop();
21 | }
22 |
23 | return new Promise(promise);
24 | }
25 |
26 | return this._toResolve.push(promise);
27 | }
28 |
29 | mapPromises() {
30 | return this._toResolve.map((promise) => new Promise(promise));
31 | }
32 |
33 | async render(Handler, flux, force = false) {
34 | if (process.env.BROWSER && !force) {
35 | debug('dev')('`altResolver.render` should not be used in browser, something went wrong');
36 | return null;
37 | }
38 |
39 | let content;
40 | try {
41 | // Fire first render to collect XHR promises
42 | debug('dev')('first render');
43 | ReactDOM.renderToString(Handler);
44 |
45 | // Get the promises collected from the first rendering
46 | const promises = this.mapPromises();
47 |
48 | // Resolve all promises collected
49 | await Promise.all(promises);
50 |
51 | debug('dev')('second render');
52 | // Get the new content with promises resolved
53 |
54 | const fluxSnapshot = flux.takeSnapshot();
55 | const app = ReactDOM.renderToString(Handler);
56 | const { title } = flux.getStore('page-title').getState();
57 |
58 | // Render the html with state in it
59 | content = { body: Iso.render(app, fluxSnapshot), title };
60 | } catch (error) {
61 | // catch script error, render 500 page
62 | debug('koa')('`rendering error`');
63 | debug('koa')(error);
64 |
65 | const fluxSnapshot = flux.takeSnapshot();
66 | const app = ReactDOM.renderToString(React.createElement(ErrorPage));
67 | const { title } = flux.getStore('page-title').getState();
68 |
69 | content = { body: Iso.render(app, fluxSnapshot), title };
70 | }
71 |
72 | // return the content
73 | return content;
74 | }
75 |
76 | }
77 |
78 | export default AltResolver;
79 |
--------------------------------------------------------------------------------
/app/utils/flux.js:
--------------------------------------------------------------------------------
1 | import Alt from 'alt';
2 | import AltResolver from './alt-resolver';
3 |
4 | class Flux extends Alt {
5 |
6 | constructor(config = {}) {
7 | super(config);
8 |
9 | this._resolver = new AltResolver();
10 |
11 | ['requests', 'locale', 'users', 'page-title', 'notifications', 'notification']
12 | .map(this.registerCouple);
13 | }
14 |
15 | registerCouple = ::this.registerCouple
16 | registerCouple(name) {
17 | this.addActions(name, require(`actions/${name}`));
18 | this.addStore(name, require(`stores/${name}`));
19 | }
20 |
21 | resolve(result) {
22 | this._resolver.resolve(result);
23 | }
24 |
25 | render(handler) {
26 | return this._resolver.render(handler, this);
27 | }
28 | }
29 |
30 | export default Flux;
31 |
--------------------------------------------------------------------------------
/app/utils/image-resolver.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | export default (imagePath) => {
4 | if (process.env.BROWSER) {
5 | debug('dev')('`image-resolver` should not be used in browser, something went wrong');
6 | throw new Error('image-resolver called on browser');
7 | } else {
8 | // Load images compiled from `webpack-stats`
9 | // don't cache the `webpack-stats.json` on dev
10 | // so we gonna read the file on each request
11 | let images;
12 | if (process.env.NODE_ENV === 'development') {
13 | const fs = require('fs');
14 | const path = require('path');
15 | images = fs.readFileSync(path.resolve(__dirname, '../../server/webpack-stats.json'));
16 | images = JSON.parse(images).images;
17 | } else {
18 | // on production, use simple `require` to cache the file
19 | images = require('../../server/webpack-stats.json').images;
20 | }
21 |
22 | // Find the correct image
23 | const regex = new RegExp(`${imagePath}$`);
24 | const image = images.find(img => regex.test(img.original));
25 |
26 | // Serve image.
27 | if (image) return image.compiled;
28 |
29 | // Serve a not-found asset maybe?
30 | return '';
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/utils/intl-loader.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | const loaders = {
4 | en(callback, force = false) {
5 | if (!window.Intl || force) {
6 | require.ensure([
7 | 'intl',
8 | 'intl/locale-data/jsonp/en.js',
9 | 'data/en'
10 | ], (require) => {
11 | require('intl');
12 | require('intl/locale-data/jsonp/en.js');
13 | const lang = require('data/en');
14 | return callback(lang);
15 | });
16 | } else {
17 | require.ensure(
18 | ['data/en'],
19 | (require) => callback(require('data/en'))
20 | );
21 | }
22 | },
23 |
24 | fr(callback, force = false) {
25 | if (!window.Intl || force) {
26 | require.ensure([
27 | 'intl',
28 | 'intl/locale-data/jsonp/fr.js',
29 | 'data/fr'
30 | ], (require) => {
31 | require('intl');
32 | require('intl/locale-data/jsonp/fr.js');
33 | const lang = require('data/fr');
34 | return callback(lang);
35 | });
36 | } else {
37 | require.ensure(
38 | ['data/fr'],
39 | (require) => callback(require('data/fr'))
40 | );
41 | }
42 | }
43 |
44 | };
45 |
46 | export default (locale, force) => {
47 | debug('dev')(`loading lang ${locale}`);
48 | return new Promise((resolve) => {
49 | return loaders[locale]((result) => {
50 | // We need to define `ReactIntl` on the global scope
51 | // in order to load specific locale data from `ReactIntl`
52 | // see: https://github.com/iam4x/isomorphic-flux-boilerplate/issues/64
53 | if (process.env.BROWSER) window.ReactIntl = require('react-intl');
54 | return resolve(result);
55 | }, force);
56 | });
57 | };
58 |
--------------------------------------------------------------------------------
/app/utils/intl-polyfill.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | const hasBuiltInLocaleData = (locale) => {
4 | return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale && Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale;
5 | };
6 |
7 | export default (locales) => {
8 | if (!process.env.BROWSER) {
9 | if (global.Intl) {
10 | if (!locales.every(hasBuiltInLocaleData)) {
11 | // `Intl` exists, but it doesn't have the data we need, so load the
12 | // polyfill and replace the constructors with need with the polyfill's.
13 | const IntlPolyfill = require('intl');
14 | Intl.NumberFormat = IntlPolyfill.NumberFormat;
15 | Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
16 | }
17 | } else {
18 | // No `Intl`: use and load polyfill
19 | global.Intl = require('intl');
20 | debug('koa')('Intl is not supported, so the polyfill has been loaded');
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/app/utils/localized-routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router';
3 |
4 | // Return an array of different routes paths for a same component
5 | // used to generate localized routes in `/app/routes.js`
6 | export function generateRoute({ paths, component }) {
7 | return paths.map(function(path) {
8 | const props = { key: path, path, component };
9 | // Static `onEnter` is defined on
10 | // component, we should pass it to route props
11 | if (component.onEnter) props.onEnter = component.onEnter;
12 | return ;
13 | });
14 | }
15 |
16 | // Replace params in route format: `/profile/:seed`
17 | //
18 | // Params:
19 | // - route: string = route with `:` preceding params
20 | // - params: object = key are param names
21 | //
22 | // Example:
23 | // For route: `/route/:foo` params will be `{foo: 'bar'}`
24 | export function replaceParams(route, params) {
25 | let parsedRoute = route.trim();
26 | Object.keys(params).forEach(function(paramKey) {
27 | const param = ':' + paramKey;
28 | const paramValue = params[paramKey];
29 | if (parsedRoute && parsedRoute.match(param)) {
30 | parsedRoute = parsedRoute.replace(param, paramValue);
31 | }
32 | });
33 | return parsedRoute;
34 | }
35 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | webapp:
2 | build: .
3 | working_dir: /src/app
4 | volumes:
5 | - ./:/src/app
6 | ports:
7 | - '3000:3000' # api/koajs server
8 | - '3001:3001' # webpack-dev-serer
9 | - '3002:3002' # browsersync app
10 | - '3003:3003' # browsersync admin
11 | environment:
12 | VIRTUAL_HOST: webapp.docker
13 | VIRTUAL_PORT: 3002
14 | command: npm run dev
15 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | var webpack = require('webpack');
4 | var cssnext = require('cssnext');
5 |
6 | var coverage;
7 | var reporters;
8 | if (process.env.CONTINUOUS_INTEGRATION) {
9 | coverage = {
10 | type: 'lcov',
11 | dir: 'coverage/'
12 | };
13 | reporters = ['coverage', 'coveralls'];
14 | }
15 | else {
16 | coverage = {
17 | type: 'html',
18 | dir: 'coverage/'
19 | };
20 | reporters = ['progress', 'coverage'];
21 | }
22 |
23 | module.exports = function (config) {
24 | config.set({
25 | browsers: ['Firefox'],
26 | browserNoActivityTimeout: 30000,
27 | frameworks: ['mocha', 'chai', 'sinon-chai'],
28 | files: ['tests.webpack.js'],
29 | preprocessors: {'tests.webpack.js': ['webpack', 'sourcemap']},
30 | reporters: reporters,
31 | coverageReporter: coverage,
32 | webpack: {
33 | devtool: 'inline-source-map',
34 | module: {
35 | loaders: [
36 | // TODO: fix sourcemaps
37 | // see: https://github.com/deepsweet/isparta-loader/issues/1
38 | {
39 | test: /\.js$|.jsx$/,
40 | loader: 'babel',
41 | exclude: /node_modules/
42 | },
43 | {
44 | test: /\.js$|.jsx$/,
45 | loader: 'isparta?{babel: {stage: 0}}',
46 | exclude: /node_modules|test|utils/
47 | },
48 | {
49 | test: /\.css$/,
50 | loader: 'style!css!postcss'
51 | },
52 | {
53 | test: /\.(jpe?g|png|gif|svg|woff|eot|ttf)$/,
54 | loader: 'file?name=[sha512:hash:base64:7].[ext]'
55 | },
56 | {
57 | test: /\.json$/, loader: 'json'
58 | }
59 | ]
60 | },
61 | plugins: [
62 | new webpack.DefinePlugin({
63 | 'process.env': {
64 | BROWSER: JSON.stringify(true),
65 | NODE_ENV: JSON.stringify('test')
66 | }
67 | })
68 | ],
69 | postcss: [
70 | cssnext()
71 | ],
72 | resolve: {
73 | extensions: ['', '.js', '.json', '.jsx'],
74 | modulesDirectories: ['node_modules', 'app']
75 | }
76 | },
77 | webpackServer: {
78 | noInfo: true
79 | }
80 | });
81 | };
82 |
--------------------------------------------------------------------------------
/logs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/igorprado/isomorphic-react-app/b049ed23849c0f5b52dff48a240b622ff2d4c5ba/logs/.gitkeep
--------------------------------------------------------------------------------
/main-heroku.js:
--------------------------------------------------------------------------------
1 | /* eslint-env es6:false */
2 |
3 | var pm2 = require('pm2');
4 |
5 | var instances = process.env.WEB_CONCURRENCY || -1; // Set by Heroku or -1 to scale to max cpu core -1
6 | var maxMemory = process.env.WEB_MEMORY || 512; // " " "
7 |
8 | pm2.connect(function() {
9 | pm2.start({
10 | script: './server/index.js',
11 | name: 'production-app', // ----> THESE ATTRIBUTES ARE OPTIONAL:
12 | exec_mode: 'cluster', // ----> https://github.com/Unitech/PM2/blob/master/ADVANCED_README.md#schema
13 | instances: instances,
14 | max_memory_restart: maxMemory + 'M', // Auto restart if process taking more than XXmo
15 | env: { // If needed declare some environment variables
16 | 'NODE_ENV': 'production'
17 | }
18 | }, function(err) {
19 | if (err) return console.error('Error while launching applications', err.stack || err);
20 | console.log('PM2 and application has been succesfully started');
21 |
22 | // Display logs in standard output
23 | pm2.launchBus(function(_err, bus) {
24 | console.log('[PM2] Log streaming started');
25 |
26 | bus.on('log:out', function(packet) {
27 | console.log('[App:%s] %s', packet.process.name, packet.data);
28 | });
29 |
30 | bus.on('log:err', function(packet) {
31 | console.error('[App:%s][Err] %s', packet.process.name, packet.data);
32 | });
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isomorphic-flux-boilerplate",
3 | "version": "0.2.0",
4 | "description": "An ES7 isomorphic Flux/ReactJS boilerplate",
5 | "main": "server/index.js",
6 | "scripts": {
7 | "build": "webpack --stats --config ./webpack/prod.config.js",
8 | "dev": "BABEL_ENV=browser NODE_ENV=development babel-node ./webpack/dev-server.js",
9 | "test": "karma start --single-run",
10 | "dev-test": "karma start",
11 | "lint": "eslint app server webpack --ext .jsx,.js",
12 | "postinstall": "npm run build"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/iam4x/isomorphic-flux-boilerplate.git"
17 | },
18 | "homepage": "https://github.com/iam4x/isomorphic-flux-boilerplate",
19 | "bugs": "https://github.com/iam4x/isomorphic-flux-boilerplate/issues",
20 | "author": "iam4x",
21 | "license": "MIT",
22 | "dependencies": {
23 | "alt": "^0.17.1",
24 | "babel": "^5.8.3",
25 | "blocked": "^1.1.0",
26 | "classnames": "^2.1.3",
27 | "cookies-js": "^1.2.1",
28 | "debug": "^2.2.0",
29 | "font-awesome": "^4.4.0",
30 | "history": "^1.9.1",
31 | "intl": "^1.0.0",
32 | "iso": "^4.1.0",
33 | "koa": "^1.0.0",
34 | "koa-cash": "^1.0.1",
35 | "koa-compressor": "^1.0.3",
36 | "koa-conditional-get": "^1.0.3",
37 | "koa-etag": "^2.0.0",
38 | "koa-favicon": "^1.2.0",
39 | "koa-hbs": "^0.7.0",
40 | "koa-helmet": "^0.2.0",
41 | "koa-logger": "^1.3.0",
42 | "koa-mount": "^1.3.0",
43 | "koa-proxy": "^0.4.0",
44 | "koa-response-time": "^1.0.2",
45 | "koa-static-cache": "^3.1.2",
46 | "lodash": "^3.10.0",
47 | "normalize.css": "^3.0.3",
48 | "pm2": "^0.15.7",
49 | "react": "^0.14.0",
50 | "react-dom": "^0.14.0",
51 | "react-intl": "^1.2.0",
52 | "react-notification-system": "^0.2.3",
53 | "react-router": "^1.0.0-rc1",
54 | "shortid": "^2.2.2"
55 | },
56 | "devDependencies": {
57 | "babel-core": "^5.8.3",
58 | "babel-eslint": "^4.0.5",
59 | "babel-loader": "^5.3.2",
60 | "babel-plugin-react-transform": "^1.0.3",
61 | "babel-runtime": "^5.8.24",
62 | "browser-sync": "^2.8.0",
63 | "css-loader": "^0.19.0",
64 | "cssnext": "^1.8.4",
65 | "del": "^2.0.2",
66 | "dev-ip": "^1.0.1",
67 | "eslint": "^1.4.1",
68 | "eslint-config-airbnb": "^0.1.0",
69 | "eslint-loader": "^1.0.0",
70 | "eslint-plugin-react": "^3.0.0",
71 | "extract-text-webpack-plugin": "^0.8.2",
72 | "file-loader": "^0.8.4",
73 | "image-webpack-loader": "^1.6.1",
74 | "imagemin": "^3.2.0",
75 | "isparta-loader": "^1.0.0",
76 | "json-loader": "^0.5.2",
77 | "karma": "^0.13.3",
78 | "karma-chai-plugins": "^0.6.0",
79 | "karma-cli": "^0.1.0",
80 | "karma-coverage": "^0.5.2",
81 | "karma-coveralls": "^1.1.2",
82 | "karma-firefox-launcher": "^0.1.6",
83 | "karma-mocha": "^0.2.0",
84 | "karma-sourcemap-loader": "^0.3.5",
85 | "karma-webpack": "^1.7.0",
86 | "koa-webpack-dev-middleware": "^1.0.1",
87 | "koa-webpack-hot-middleware": "^1.0.1",
88 | "mocha": "^2.2.5",
89 | "node-watch": "^0.3.4",
90 | "postcss-loader": "^0.5.1",
91 | "react-addons-test-utils": "^0.14.0-rc1",
92 | "react-hot-loader": "^1.2.8",
93 | "react-transform-catch-errors": "^1.0.0",
94 | "react-transform-hmr": "^1.0.0",
95 | "redbox-react": "^1.0.4",
96 | "style-loader": "^0.12.3",
97 | "webpack": "^1.10.5",
98 | "webpack-hot-middleware": "^2.0.2"
99 | },
100 | "tags": [
101 | "react",
102 | "flux",
103 | "alt",
104 | "iso",
105 | "isomorphic",
106 | "spa",
107 | "ssr",
108 | "koa",
109 | "webpack",
110 | "jsx",
111 | "es6",
112 | "babel",
113 | "sass",
114 | "scss",
115 | "eslint",
116 | "react-router",
117 | "react-hot-loader",
118 | "hot-loader",
119 | "async",
120 | "server",
121 | "generator",
122 | "es7",
123 | "i18n",
124 | "intl"
125 | ],
126 | "keywords": [
127 | "react",
128 | "flux",
129 | "alt",
130 | "iso",
131 | "isomorphic",
132 | "spa",
133 | "ssr",
134 | "koa",
135 | "webpack",
136 | "jsx",
137 | "es6",
138 | "babel",
139 | "sass",
140 | "scss",
141 | "eslint",
142 | "react-router",
143 | "react-hot-loader",
144 | "hot-loader",
145 | "async",
146 | "server",
147 | "generator",
148 | "es7",
149 | "i18n",
150 | "intl"
151 | ],
152 | "engines": {
153 | "nodejs": "^4.0.0"
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/processes.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "app",
5 | "script": "./server/index.js",
6 | "instances": 1,
7 | "log_date_format": "YYYY-MM-DD HH:mm Z",
8 | "log_file": "./logs/app.log",
9 | "error_file": "./logs/app-err.log",
10 | "out_file": "./logs/app-out.log",
11 | "exec_mode": "cluster_mode",
12 | "max_memory_restart": "500M",
13 | "watch": false,
14 | "env": {
15 | "NODE_ENV": "production",
16 | "DEBUG": "koa"
17 | },
18 | "node_args": "--max_old_space_size=300 --harmony"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/server/config/all.json:
--------------------------------------------------------------------------------
1 | {
2 | "locales": ["en", "fr"]
3 | }
4 |
--------------------------------------------------------------------------------
/server/config/development.js:
--------------------------------------------------------------------------------
1 | export default {
2 | port: parseInt(process.env.PORT, 10) || 3000
3 | };
4 |
--------------------------------------------------------------------------------
/server/config/init.js:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | import baseConfig from './all.json';
4 | const { NODE_ENV = 'development' } = process.env;
5 | let config;
6 |
7 | try {
8 | config = require(`./${NODE_ENV}`);
9 | } catch (error) {
10 | debug('dev')(`No specific configuration for env ${NODE_ENV}`);
11 | }
12 |
13 | export default { ...baseConfig, ...config };
14 |
--------------------------------------------------------------------------------
/server/config/production.js:
--------------------------------------------------------------------------------
1 | export default {
2 | port: parseInt(process.env.PORT, 10) || 3010
3 | };
4 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | // Delete the `BROWSER` env variable if it's present
2 | // https://github.com/iam4x/isomorphic-flux-boilerplate/issues/16
3 | delete process.env.BROWSER;
4 |
5 | // Tell `require` calls to look into `/app` also
6 | // it will avoid `../../../../../` require strings
7 | process.env.NODE_PATH = 'app';
8 | require('module').Module._initPaths();
9 |
10 | // Install `babel` hook for ES6
11 | require('babel/register');
12 |
13 | // Load Intl polyfill
14 | require('utils/intl-polyfill')(require('./config/init').locales);
15 |
16 | // Start the server
17 | require('./koa.js');
18 |
--------------------------------------------------------------------------------
/server/koa.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import debug from 'debug';
3 |
4 | import koa from 'koa';
5 | import hbs from 'koa-hbs';
6 | import mount from 'koa-mount';
7 | import helmet from 'koa-helmet';
8 | import logger from 'koa-logger';
9 | import favicon from 'koa-favicon';
10 | import staticCache from 'koa-static-cache';
11 | import responseTime from 'koa-response-time';
12 |
13 | import router from './router';
14 | import config from './config/init';
15 |
16 | const app = koa();
17 | const env = process.env.NODE_ENV || 'development';
18 |
19 | // add header `X-Response-Time`
20 | app.use(responseTime());
21 | app.use(logger());
22 |
23 | // various security headers
24 | app.use(helmet());
25 |
26 | if (env === 'production') {
27 | // set debug env to `koa` only
28 | // must be set programmaticaly for windows
29 | debug.enable('koa');
30 |
31 | // load production middleware
32 | app.use(require('koa-conditional-get')());
33 | app.use(require('koa-etag')());
34 | app.use(require('koa-compressor')());
35 | }
36 |
37 | if (env === 'development') {
38 | // set debug env, must be programmaticaly for windows
39 | debug.enable('dev,koa');
40 | // log when process is blocked
41 | require('blocked')((ms) => debug('koa')(`blocked for ${ms}ms`));
42 | }
43 |
44 | app.use(favicon(path.join(__dirname, '../app/images/favicon.ico')));
45 | app.use(hbs.middleware({
46 | defaultLayout: 'index',
47 | layoutsPath: path.join(__dirname, '/views/layouts'),
48 | viewPath: path.join(__dirname, '/views')
49 | }));
50 |
51 | const cacheOpts = { maxAge: 86400000, gzip: true };
52 |
53 | // Proxy asset folder to webpack development server in development mode
54 | if (env === 'development') {
55 | const webpackConfig = require('./../webpack/dev.config');
56 | const proxy = require('koa-proxy')({
57 | host: 'http://0.0.0.0:' + webpackConfig.server.port,
58 | map: (filePath) => 'assets/' + filePath
59 | });
60 | app.use(mount('/assets', proxy));
61 | } else {
62 | app.use(mount('/assets', staticCache(path.join(__dirname, '../dist'), cacheOpts)));
63 | }
64 |
65 | app.use(router);
66 | app.listen(config.port);
67 |
68 | // Tell parent process koa-server is started
69 | if (process.send) process.send('online');
70 | debug('koa')(`Application started on port ${config.port}`);
71 |
--------------------------------------------------------------------------------
/server/router.jsx:
--------------------------------------------------------------------------------
1 | import debug from 'debug';
2 |
3 | import React from 'react';
4 | import { RoutingContext, match } from 'react-router';
5 |
6 | import createLocation from 'history/lib/createLocation';
7 |
8 | // Paths are relative to `app` directory
9 | import routes from 'routes';
10 | import Flux from 'utils/flux';
11 |
12 | // We need wrap `Router.run` into a promise
13 | // in order to use the keyword `yield` and keep
14 | // the correct way `koajs` works
15 | const promisifiedRouter = (location) =>
16 | new Promise((resolve) =>
17 | match({ routes, location }, (...args) => resolve(args)));
18 |
19 | export default function *() {
20 | // Init alt instance
21 | const flux = new Flux();
22 |
23 | // Get request locale for rendering
24 | const locale = this.cookies.get('_lang') || this.acceptsLanguages(require('./config/init').locales) || 'en';
25 | const { messages } = require(`data/${locale}`);
26 |
27 | // Populate store with locale
28 | flux
29 | .getActions('locale')
30 | .switchLocaleSuccess({ locale, messages });
31 |
32 | debug('dev')(`locale of request: ${locale}`);
33 |
34 | try {
35 | // Pass correct location of the request to `react-router`
36 | // it will return the matched components for the route into `initialState`
37 | const location = createLocation(this.request.path, this.request.query);
38 | const [ error, redirect, renderProps ] = yield promisifiedRouter(location);
39 |
40 | // Render 500 error page from server
41 | if (error) throw error;
42 |
43 | // Handle component `onEnter` transition
44 | if (redirect) {
45 | const { pathname, search } = redirect;
46 | return this.redirect(pathname + search);
47 | }
48 |
49 | // Render application of correct location
50 | // We need to re-define `createElement` of `react-router`
51 | // in order to include `flux` on children components props
52 | const routerProps = {
53 | ...renderProps, location,
54 | createElement: (component, props) => {
55 | // Take locale and messages from `locale` store
56 | // and pass them to every components rendered from `Router`
57 | const i18n = flux.getStore('locale').getState();
58 | return React.createElement(component, { ...props, ...i18n, flux });
59 | }
60 | };
61 |
62 | // Use `alt-resolver` to render component with fetched data
63 | const { body, title } = yield flux.render();
64 |
65 | // Assets name are found into `webpack-stats`
66 | const assets = require('./webpack-stats.json');
67 |
68 | // Don't cache assets name on dev
69 | if (process.env.NODE_ENV === 'development') {
70 | delete require.cache[require.resolve('./webpack-stats.json')];
71 | }
72 |
73 | debug('dev')('return html content');
74 | yield this.render('main', { body, assets, locale, title });
75 | } catch (error) {
76 | // Catch error from rendering procress
77 | // In other cases just return the error
78 | throw error;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/views/layouts/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 | {{#each assets.style}}
13 |
14 | {{/each}}
15 | {{{title}}}
16 |
17 |
18 | {{{body}}}
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/server/views/main.hbs:
--------------------------------------------------------------------------------
1 | {{{body}}}
2 |
--------------------------------------------------------------------------------
/test/spec/components/app.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 | import Flux from 'utils/flux';
5 |
6 | import reactRouterStub from '../../utils/stub-router-context';
7 | import injectLang from '../../utils/inject-lang';
8 |
9 | import App from 'components/app';
10 |
11 | const should = chai.should();
12 |
13 | describe('App', () => {
14 | let node;
15 | let instance;
16 | let flux;
17 |
18 | // Inject language
19 | beforeEach(() => {
20 | flux = new Flux();
21 | injectLang(flux);
22 | });
23 |
24 | beforeEach(() => {
25 | const Stubbed = reactRouterStub(App, {flux});
26 | const element = React.createElement(Stubbed);
27 | node = window.document.createElement('div');
28 | instance = ReactDOM.render(element, node);
29 | });
30 |
31 | afterEach(function() {
32 | if (instance) ReactDOM.unmountComponentAtNode(node);
33 | });
34 |
35 | it('should render header correctly', () => {
36 | const header = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'header');
37 | header.length.should.eql(1);
38 | });
39 |
40 | it('should render logo correctly', () => {
41 | const logo = TestUtils.findRenderedDOMComponentWithClass(instance, 'app--logo');
42 | should.exist(logo);
43 | });
44 |
45 | it('should change page title', function() {
46 | flux.getActions('page-title').set('foobar');
47 | document.title.should.eql('ISO-ReactJS | foobar');
48 | });
49 |
50 | it('should handle locale change', function(done) {
51 | const handleChange = function({locales}) {
52 | locales[0].should.eql('fr');
53 | flux.getStore('locale').getLocale().should.eql('fr');
54 | flux.getStore('locale').unlisten(handleChange);
55 | return done();
56 | };
57 | flux.getStore('locale').listen(handleChange);
58 | flux.getActions('locale').switchLocale('fr');
59 | });
60 |
61 | it('should render children component', function() {
62 | ReactDOM.unmountComponentAtNode(node);
63 | const Stubbed = reactRouterStub(App, {flux});
64 | const Element = foobar
;
65 | node = window.document.createElement('div');
66 | instance = ReactDOM.render(Element, node);
67 |
68 | const title = TestUtils.findRenderedDOMComponentWithClass(instance, 'foobar');
69 | should.exist(title);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/spec/components/header.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 | import Flux from 'utils/flux';
5 | import objectAssign from 'react/lib/Object.assign';
6 |
7 | import reactRouterStub from '../../utils/stub-router-context';
8 | import injectLang from '../../utils/inject-lang';
9 |
10 | import Header from 'components/header';
11 |
12 | chai.should();
13 |
14 | describe('Header', () => {
15 | let node;
16 | let instance;
17 | let flux;
18 |
19 | beforeEach(() => {
20 | flux = new Flux();
21 |
22 | const props = objectAssign({flux}, injectLang(flux));
23 | const Stubbed = reactRouterStub(Header, props);
24 |
25 | node = window.document.createElement('div');
26 | instance = ReactDOM.render(React.createElement(Stubbed), node);
27 | });
28 |
29 | afterEach(() => {
30 | if (instance) ReactDOM.unmountComponentAtNode(node);
31 | });
32 |
33 | it('should render links correctly', () => {
34 | const links = TestUtils.findRenderedDOMComponentWithClass(instance, 'app--navbar');
35 | links.children.length.should.eql(3);
36 | });
37 |
38 | it('should render lang picker correctly', () => {
39 | const langs = TestUtils.findRenderedDOMComponentWithClass(instance, 'lang--picker');
40 | langs.children.length.should.eql(2);
41 | });
42 |
43 | it('should handle requests change', function() {
44 | flux.getActions('requests').start();
45 | const spinner = TestUtils.findRenderedDOMComponentWithClass(instance, 'app--spinner');
46 | spinner.className.indexOf('active').should.not.eql(-1);
47 |
48 | flux.getActions('requests').fail();
49 | spinner.className.indexOf('active').should.eql(-1);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/test/spec/components/lang-picker.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 | import Flux from 'utils/flux';
5 |
6 | import injectLang from '../../utils/inject-lang';
7 |
8 | import LangPicker from 'components/shared/lang-picker';
9 |
10 | const should = chai.should();
11 |
12 | describe('LangPicker', () => {
13 | let node;
14 | let instance;
15 | let flux;
16 | let spy;
17 |
18 | beforeEach(() => {
19 | flux = new Flux();
20 | spy = sinon.spy();
21 |
22 | const props = Object.assign(
23 | {activeLocale: 'en', onChange: spy},
24 | injectLang(flux)
25 | );
26 |
27 | const element = React.createElement(LangPicker, props);
28 |
29 | node = window.document.createElement('div');
30 | instance = ReactDOM.render(element, node);
31 | });
32 |
33 | afterEach(function() {
34 | if (instance) ReactDOM.unmountComponentAtNode(node);
35 | });
36 |
37 | it('should have en locale active', () => {
38 | const active = TestUtils.findRenderedDOMComponentWithClass(instance, 'active');
39 | should.exist(active);
40 | active.innerHTML.should.eql('en');
41 | });
42 |
43 | it('should call `onChange` handler', function() {
44 | const locales = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'a');
45 | locales.length.should.eql(2);
46 |
47 | const inactive = locales.find(l => !l.className);
48 | TestUtils.Simulate.click(inactive);
49 |
50 | spy.should.have.been.calledOnce;
51 | spy.should.have.been.calledWith('fr');
52 | });
53 |
54 | it('should do nothing on same locale click', () => {
55 | const active = TestUtils.findRenderedDOMComponentWithClass(instance, 'active');
56 | should.exist(active);
57 | active.innerHTML.should.eql('en');
58 | TestUtils.Simulate.click(active);
59 | active.className.should.eql('active');
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/test/spec/components/profile.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 | import objectAssign from 'react/lib/Object.assign';
5 | import {capitalize} from 'lodash';
6 |
7 | import reactRouterStub from '../../utils/stub-router-context';
8 | import injectLang from '../../utils/inject-lang';
9 |
10 | import Flux from 'utils/flux';
11 | import Profile from 'components/profile';
12 |
13 | import {users} from 'data/users.json';
14 |
15 | const should = chai.should();
16 | const seed = '7729a1ef4ba6ef68';
17 |
18 | describe('Profile', () => {
19 | let flux;
20 | let node;
21 | let instance;
22 |
23 | beforeEach(() => {
24 | flux = new Flux();
25 | node = window.document.createElement('div');
26 |
27 | const props = objectAssign({params: {seed}}, {flux}, injectLang(flux));
28 | const Stubbed = reactRouterStub(Profile, props);
29 |
30 | instance = ReactDOM.render(React.createElement(Stubbed), node);
31 | });
32 |
33 | afterEach(function() {
34 | if (instance) ReactDOM.unmountComponentAtNode(node);
35 | });
36 |
37 | it('should render user name after request', (done) => {
38 | const handleChange = () => {
39 | const fullName = TestUtils.findRenderedDOMComponentWithTag(instance, 'h2');
40 | should.exist(fullName);
41 | const user = users.find((u) => u.seed === seed);
42 | should.exist(user.user);
43 | fullName.textContent.should.eql(`${capitalize(user.user.name.first)} ${capitalize(user.user.name.last)}`);
44 | flux.getStore('users').unlisten(handleChange);
45 | return done();
46 | };
47 | flux.getStore('users').listen(handleChange);
48 | });
49 |
50 | it('should render user picture after request', (done) => {
51 | const handleChange = () => {
52 | const picture = TestUtils.findRenderedDOMComponentWithTag(instance, 'img');
53 | should.exist(picture);
54 | const user = users.find((u) => u.seed === seed);
55 | should.exist(user.user);
56 | picture.src.should.eql(user.user.picture.medium);
57 | flux.getStore('users').unlisten(handleChange);
58 | return done();
59 | };
60 | flux.getStore('users').listen(handleChange);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/test/spec/components/users.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 | import Flux from 'utils/flux';
5 | import objectAssign from 'react/lib/Object.assign';
6 |
7 | import reactRouterStub from '../../utils/stub-router-context';
8 | import injectLang from '../../utils/inject-lang';
9 |
10 | import Users from 'components/users';
11 |
12 | const should = chai.should();
13 |
14 | describe('Users', () => {
15 | let node;
16 | let instance;
17 | let flux;
18 |
19 | beforeEach(() => {
20 | flux = new Flux();
21 |
22 | const props = objectAssign({flux}, injectLang(flux));
23 | const Stubbed = reactRouterStub(Users, props);
24 |
25 | node = window.document.createElement('div');
26 | instance = ReactDOM.render(React.createElement(Stubbed), node);
27 | });
28 |
29 | afterEach(() => {
30 | if (instance) {
31 | ReactDOM.unmountComponentAtNode(node);
32 | }
33 | });
34 |
35 | it('should render correctly', () => {
36 | const {messages} = flux.getStore('locale').getState();
37 | const title = TestUtils.findRenderedDOMComponentWithTag(instance, 'h1');
38 | title.textContent.should.eql(messages.users.title);
39 | });
40 |
41 | it('should render without users', () => {
42 | // Check `` don't exists
43 | const td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row');
44 | td.should.be.empty;
45 | });
46 |
47 | it('should render 10 users after first fetch', (done) => {
48 | const handleChange = () => {
49 | const td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row');
50 | td.length.should.eql(10);
51 | flux.getStore('users').unlisten(handleChange);
52 | return done();
53 | };
54 | flux.getStore('users').listen(handleChange);
55 | });
56 |
57 | it('should add an user after click on add button', (done) => {
58 | const handleAddChange = () => {
59 | // 11 users after add
60 | let td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row');
61 | td.length.should.eql(11);
62 |
63 | // clean
64 | flux.getStore('users').unlisten(handleAddChange);
65 | return done();
66 | };
67 |
68 | const handleFetchChange = () => {
69 | // 10 users after fetch
70 | let td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row');
71 | td.length.should.eql(10);
72 |
73 | // clean
74 | flux.getStore('users').unlisten(handleFetchChange);
75 |
76 | // add an user
77 | flux.getStore('users').listen(handleAddChange);
78 | const addButton = TestUtils.findRenderedDOMComponentWithClass(instance, 'add--button');
79 | should.exist(addButton);
80 |
81 | setTimeout(() => {
82 | TestUtils.Simulate.click(addButton);
83 | }, 0);
84 | };
85 |
86 | flux.getStore('users').listen(handleFetchChange);
87 | });
88 |
89 | it('should remove an user', (done) => {
90 | const handleChange = () => {
91 | // 10 users after fetch
92 | let td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row');
93 | td.length.should.eql(10);
94 |
95 | // remove an user
96 | const removeButton = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--remove')[0];
97 | should.exist(removeButton);
98 |
99 | // wait for dispatch to be done before
100 | // calling another action
101 | setTimeout(() => {
102 | TestUtils.Simulate.click(removeButton);
103 |
104 | // it should have 9 users
105 | td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row');
106 | td.length.should.eql(9);
107 |
108 | // clean
109 | flux.getStore('users').unlisten(handleChange);
110 | return done();
111 | }, 0);
112 | };
113 | flux.getStore('users').listen(handleChange);
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/test/spec/pages/not-found.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 |
5 | import NotFound from 'pages/not-found';
6 |
7 | chai.should();
8 |
9 | describe('NotFoundPage', () => {
10 | let instance;
11 | let node;
12 |
13 | beforeEach(() => {
14 | node = window.document.createElement('div');
15 | instance = ReactDOM.render(, node);
16 | });
17 |
18 | afterEach(() => {
19 | if (instance) ReactDOM.unmountComponentAtNode(node);
20 | });
21 |
22 | it('should render correctly', () => {
23 | const title = TestUtils.findRenderedDOMComponentWithTag(instance, 'h1');
24 | title.textContent.should.eql('404');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/spec/pages/server-error.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 |
5 | import ServerError from 'pages/server-error';
6 |
7 | chai.should();
8 |
9 | describe('ErrorPage', () => {
10 | let instance;
11 | let node;
12 |
13 | beforeEach(() => {
14 | node = window.document.createElement('div');
15 | instance = ReactDOM.render(, node);
16 | });
17 |
18 | afterEach(() => {
19 | if (instance) ReactDOM.unmountComponentAtNode(node);
20 | });
21 |
22 | it('should render correctly', () => {
23 | const title = TestUtils.findRenderedDOMComponentWithTag(instance, 'h1');
24 | title.textContent.should.eql('500');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/spec/stores/users.test.js:
--------------------------------------------------------------------------------
1 | import Flux from 'utils/flux';
2 |
3 | chai.should();
4 |
5 | describe('UsersStore', () => {
6 | let flux;
7 | let store;
8 | let actions;
9 |
10 | beforeEach(() => {
11 | flux = new Flux();
12 | store = flux.getStore('users');
13 | actions = flux.getActions('users');
14 | });
15 |
16 | it('should overide users on new fetch', (done) => {
17 | let count = 0;
18 | const handleChange = () => {
19 | let {users} = store.getState();
20 | users.length.should.be.eql(10);
21 |
22 | setTimeout(() => {
23 | if (++count === 2) {
24 | // clean
25 | store.unlisten(handleChange);
26 | return done();
27 | }
28 | actions.fetch();
29 | });
30 | };
31 | store.listen(handleChange);
32 | actions.fetch();
33 | });
34 |
35 | it('should merge users when on new fetch', function(done) {
36 | // add dummy user in store
37 | actions.fetchBySeedSuccess({name: 'foo', seed: 'bar'});
38 |
39 | const handleChange = function({users}) {
40 | users.length.should.be.eql(11);
41 | store.unlisten(handleChange);
42 | return done();
43 | };
44 |
45 | store.listen(handleChange);
46 | actions.fetch();
47 | });
48 |
49 | it('should update user with same seed', function() {
50 | actions.fetchBySeedSuccess({name: 'foo', seed: 'bar'});
51 |
52 | store.getState().users[0].should.eql({name: 'foo', seed: 'bar'});
53 |
54 | actions.fetchBySeedSuccess({name: 'yolo', seed: 'bar'});
55 | store.getState().users[0].should.eql({name: 'yolo', seed: 'bar'});
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/test/spec/utils/alt-resolver.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Flux from 'utils/flux';
3 | import AltResolver from 'utils/alt-resolver';
4 |
5 | import injectLang from '../../utils/inject-lang';
6 |
7 | const should = chai.should();
8 |
9 | const Dummy = React.createClass({
10 | render() {
11 | return React.createElement('h1');
12 | }
13 | });
14 |
15 | const DummyError = React.createClass({
16 | render() {
17 | throw new Error();
18 | }
19 | });
20 |
21 | describe('Alt Resolver', () => {
22 | let flux;
23 | let altResolver;
24 |
25 | beforeEach(() => {
26 | flux = new Flux();
27 | altResolver = new AltResolver();
28 | injectLang(flux);
29 | });
30 |
31 | it('should map promises on env server', () => {
32 | altResolver.mapPromises().should.be.empty;
33 | altResolver.resolve(function() {}, true);
34 | altResolver.mapPromises().should.not.be.empty;
35 | });
36 |
37 | it('should render async a dummy component', (done) => {
38 | (async function () {
39 | const content = await altResolver.render(Dummy, flux, true);
40 | should.exist(content);
41 | return done();
42 | })();
43 | });
44 |
45 | it('should not render on browser', (done) => {
46 | (async function () {
47 | const content = await altResolver.render();
48 | should.not.exist(content);
49 | return done();
50 | })();
51 | });
52 |
53 | it('should render 500 on error', (done) => {
54 | (async function () {
55 | const content = await altResolver.render(DummyError, flux, true);
56 | should.exist(content);
57 | content.body.should.have.string('500');
58 | return done();
59 | })();
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/test/spec/utils/image-resolver.test.js:
--------------------------------------------------------------------------------
1 | import imageResolver from 'utils/image-resolver';
2 |
3 | const should = chai.should();
4 |
5 | describe('ImageResolver', () => {
6 | // TODO: Set up TDD for server
7 | // `ImageResolver` is mainly used on the server
8 |
9 | it('should throw an error using on browser', () => {
10 | try {
11 | imageResolver();
12 | }
13 | catch (error) {
14 | should.exist(error);
15 | }
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/spec/utils/intl-loader.test.js:
--------------------------------------------------------------------------------
1 | import intlLoader from 'utils/intl-loader';
2 |
3 | const should = chai.should();
4 |
5 | describe('IntlLoader', () => {
6 | it('should load english lang without polyfill', (done) => {
7 | (async function () {
8 | const lang = await intlLoader('en');
9 | should.exist(lang);
10 | lang.should.be.an('object');
11 | lang.should.have.any.keys('messages');
12 | return done();
13 | })();
14 | });
15 |
16 | it('should load french lang without polyfill', (done) => {
17 | (async function () {
18 | const lang = await intlLoader('fr');
19 | should.exist(lang);
20 | lang.should.be.an('object');
21 | lang.should.have.any.keys('messages');
22 | return done();
23 | })();
24 | });
25 |
26 | it('should load english and with polyfill', (done) => {
27 | (async function () {
28 | const lang = await intlLoader('en', true);
29 | should.exist(lang);
30 | lang.should.be.an('object');
31 | lang.should.have.any.keys('messages');
32 | return done();
33 | })();
34 | });
35 |
36 | it('should load french and with polyfill', (done) => {
37 | (async function () {
38 | const lang = await intlLoader('fr', true);
39 | should.exist(lang);
40 | lang.should.be.an('object');
41 | lang.should.have.any.keys('messages');
42 | return done();
43 | })();
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/spec/utils/localized-routes.test.js:
--------------------------------------------------------------------------------
1 | import {generateRoute, replaceParams} from 'utils/localized-routes';
2 |
3 | chai.should();
4 |
5 | describe('LocalizedRoutes utils', function() {
6 | it('should return an array of routes', function() {
7 | const routes = generateRoute({
8 | paths: ['a', 'b', 'c'],
9 | component: function() {}
10 | });
11 | routes.length.should.eql(3);
12 | });
13 |
14 | it('should replace params in url', function() {
15 | const route = replaceParams('/foo/:bar/bar/:foo', {bar: 'yolo', foo: 'swag'});
16 | route.should.eql('/foo/yolo/bar/swag');
17 | });
18 |
19 | it('should ignore inexisting params', function() {
20 | const route = replaceParams('/foo', {bar: 'foo'});
21 | route.should.eql('/foo');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/utils/inject-lang.js:
--------------------------------------------------------------------------------
1 | export default (flux, locale='en') => {
2 | const {messages} = require(`data/${locale}`);
3 |
4 | flux
5 | .getActions('locale')
6 | .switchLocaleSuccess({locale, messages});
7 |
8 | return flux.getStore('locale').getState();
9 | };
10 |
--------------------------------------------------------------------------------
/test/utils/stub-router-context.jsx:
--------------------------------------------------------------------------------
1 | import objectAssign from 'react/lib/Object.assign';
2 | import React, {PropTypes} from 'react';
3 |
4 | export default function stubRouterContext(Component, props, stubs) {
5 | const RouterStub = objectAssign({
6 | makePath() {},
7 | makeHref() {},
8 | transitionTo() {},
9 | replaceWith() {},
10 | goBack() {},
11 | getCurrentPath() {},
12 | getCurrentRoutes() {},
13 | getCurrentPathname() {},
14 | getCurrentParams() {},
15 | getCurrentQuery() {},
16 | isActive() {},
17 | getRouteAtDepth() {},
18 | setRouteComponentAtDepth() {}
19 | }, stubs);
20 |
21 | return React.createClass({
22 | displayName: 'RouterStub',
23 | childContextTypes: {
24 | router: PropTypes.object.isRequired,
25 | routeDepth: PropTypes.number.isRequired
26 | },
27 | getChildContext() {
28 | return {
29 | router: RouterStub,
30 | routeDepth: 0
31 | };
32 | },
33 | render() {
34 | const customProps = Object.assign({}, this.props, props);
35 | return ;
36 | }
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/tests.webpack.js:
--------------------------------------------------------------------------------
1 | // Browser ES6 Polyfill
2 | require('babel/polyfill');
3 | const context = require.context('./test/spec', true, /\.test\.jsx$|\.test\.js$/);
4 | context.keys().forEach(context);
5 |
--------------------------------------------------------------------------------
/webpack/base.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | var path = require('path');
3 | var writeStats = require('./utils/write-stats');
4 |
5 | var JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/;
6 |
7 | module.exports = {
8 | devtool: 'source-map',
9 | entry: {
10 | app: './app/index.js'
11 | },
12 | output: {
13 | path: path.resolve(__dirname, '..', 'dist'),
14 | filename: '[name]-[hash].js',
15 | chunkFilename: '[name]-[hash].js',
16 | publicPath: '/assets/'
17 | },
18 | module: {
19 | preLoaders: [
20 | {
21 | test: JS_REGEX,
22 | exclude: /node_modules/,
23 | loader: 'eslint'
24 | }
25 | ],
26 | loaders: [
27 | {test: /\.json$/, exclude: /node_modules/, loader: 'json'},
28 | {test: JS_REGEX, exclude: /node_modules/, loader: 'babel'}
29 | ],
30 | },
31 | plugins: [
32 | // write webpack stats
33 | function() {
34 | this.plugin('done', writeStats);
35 | }
36 | ],
37 | resolve: {
38 | extensions: ['', '.js', '.json', '.jsx', '.es6', '.babel'],
39 | modulesDirectories: ['node_modules', 'app']
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/webpack/dev-server.js:
--------------------------------------------------------------------------------
1 | import koa from 'koa';
2 | import debug from 'debug';
3 | import webpack from 'webpack';
4 |
5 | import config from './dev.config';
6 |
7 | const app = koa();
8 | const compiler = webpack(config.webpack);
9 |
10 | debug.enable('dev');
11 |
12 | app.use(require('koa-webpack-dev-middleware')(compiler, config.server.options));
13 | app.use(require('koa-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 webpack from 'webpack';
2 | import cssnext from 'cssnext';
3 | import { isArray } from 'lodash';
4 |
5 | import baseConfig from './base.config';
6 | import startKoa from './utils/start-koa';
7 |
8 | const { VIRTUAL_HOST, C9_HOSTNAME } = process.env;
9 |
10 | const LOCAL_IP = require('dev-ip')();
11 |
12 | const PORT = (C9_HOSTNAME) ? '443' : parseInt(process.env.PORT, 10) + 1 || 3001;
13 | const HOST = VIRTUAL_HOST || C9_HOSTNAME || isArray(LOCAL_IP) && LOCAL_IP[0] || LOCAL_IP || 'localhost';
14 | const PUBLIC_PATH = `//${HOST}:${PORT}/assets/`;
15 |
16 | export default {
17 | server: {
18 | port: PORT,
19 | options: {
20 | publicPath: (C9_HOSTNAME) ? '/' : PUBLIC_PATH,
21 | hot: true,
22 | stats: {
23 | assets: true,
24 | colors: true,
25 | version: false,
26 | hash: false,
27 | timings: true,
28 | chunks: false,
29 | chunkModules: false
30 | }
31 | }
32 | },
33 | webpack: {
34 | ...baseConfig,
35 | devtool: 'cheap-module-source-map',
36 | entry: {
37 | app: [
38 | `webpack-hot-middleware/client?path=//${HOST}:${PORT}/__webpack_hmr`,
39 | './app/index.js'
40 | ]
41 | },
42 | ouput: { ...baseConfig.output, publicPath: PUBLIC_PATH },
43 | postcss: [ cssnext() ],
44 | module: {
45 | ...baseConfig.module,
46 | loaders: [
47 | ...baseConfig.module.loaders,
48 | {
49 | test: /\.(jpe?g|png|gif|svg|woff|eot|ttf)$/,
50 | loader: 'file?name=[sha512:hash:base64:7].[ext]',
51 | exclude: /node_modules/
52 | },
53 | {
54 | test: /\.css$/,
55 | loader: 'style!css?sourceMap!postcss',
56 | exclude: /node_modules/
57 | }
58 | ]
59 | },
60 | plugins: [
61 | // hot reload
62 | new webpack.optimize.OccurenceOrderPlugin(),
63 | new webpack.HotModuleReplacementPlugin(),
64 | new webpack.NoErrorsPlugin(),
65 |
66 | new webpack.DefinePlugin({
67 | 'process.env': {
68 | BROWSER: JSON.stringify(true),
69 | NODE_ENV: JSON.stringify('development')
70 | }
71 | }),
72 |
73 | new webpack.optimize.DedupePlugin(),
74 |
75 | ...baseConfig.plugins,
76 |
77 | function() { this.plugin('done', startKoa); }
78 | ]
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/webpack/prod.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | require('babel/register');
3 |
4 | var path = require('path');
5 | var webpack = require('webpack');
6 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
7 | var baseConfig = require('./base.config');
8 | var cssnext = require('cssnext');
9 |
10 | // clean `.tmp` && `dist`
11 | require('./utils/clean-dist')();
12 |
13 | var config = Object.assign({}, baseConfig);
14 |
15 | config.module.loaders = config.module.loaders.concat([
16 | {
17 | test: /\.(woff|eot|ttf)$/,
18 | loader: 'file?name=[sha512:hash:base64:7].[ext]'
19 | },
20 | {
21 | test: /\.(jpe?g|png|gif|svg)$/,
22 | loader: 'file?name=[sha512:hash:base64:7].[ext]!image?optimizationLevel=7&progressive&interlaced'
23 | },
24 | {
25 | test: /\.css$/,
26 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap!postcss')
27 | }
28 | ]);
29 |
30 | config.postcss = [
31 | cssnext({browsers: 'last 2 versions'})
32 | ];
33 |
34 | config.plugins = [
35 | // extract css
36 | new ExtractTextPlugin('[name]-[chunkhash].css'),
37 |
38 | // set env
39 | new webpack.DefinePlugin({
40 | 'process.env': {
41 | BROWSER: JSON.stringify(true),
42 | NODE_ENV: JSON.stringify('production')
43 | }
44 | }),
45 |
46 | // optimizations
47 | new webpack.optimize.DedupePlugin(),
48 | new webpack.optimize.OccurenceOrderPlugin(),
49 | new webpack.optimize.UglifyJsPlugin({
50 | compress: {
51 | warnings: false,
52 | screw_ie8: true,
53 | sequences: true,
54 | dead_code: true,
55 | drop_debugger: true,
56 | comparisons: true,
57 | conditionals: true,
58 | evaluate: true,
59 | booleans: true,
60 | loops: true,
61 | unused: true,
62 | hoist_funs: true,
63 | if_return: true,
64 | join_vars: true,
65 | cascade: true,
66 | drop_console: true
67 | },
68 | output: {
69 | comments: false
70 | }
71 | })
72 | ].concat(config.plugins);
73 |
74 | module.exports = config;
75 |
--------------------------------------------------------------------------------
/webpack/utils/clean-dist.js:
--------------------------------------------------------------------------------
1 | import del from 'del';
2 | import path from 'path';
3 | import debug from 'debug';
4 |
5 | export default () => {
6 | const DIST_PATH = path.resolve(__dirname, '../../dist/*');
7 | del.sync([DIST_PATH]);
8 | debug('dev')('cleaned `dist` directory');
9 | };
10 |
--------------------------------------------------------------------------------
/webpack/utils/start-koa.js:
--------------------------------------------------------------------------------
1 | import cp from 'child_process';
2 | import path from 'path';
3 | import debug from 'debug';
4 | import browserSync from 'browser-sync';
5 | import watch from 'node-watch';
6 | import { noop } from 'lodash';
7 |
8 | let server;
9 | let started;
10 | let serverReload;
11 | const KOA_PATH = path.join(__dirname, '../../server/index');
12 |
13 | const startServer = () => {
14 | // Define `restartServer`
15 | const restartServer = () => {
16 | debug('dev')('restarting koa application');
17 | serverReload = true;
18 | server.kill('SIGTERM');
19 | return startServer();
20 | };
21 |
22 | // merge env for the new process
23 | const env = { ...process.env, NODE_ENV: 'development', BABEL_ENV: 'server' };
24 | // start the server procress
25 | server = cp.fork(KOA_PATH, { env });
26 | // when server is `online`
27 | server.once('message', (message) => {
28 | if (message.match(/^online$/)) {
29 | if (serverReload) {
30 | serverReload = false;
31 | browserSync.reload();
32 | }
33 | if (!started) {
34 | started = true;
35 |
36 | // Start browserSync
37 | browserSync({
38 | port: parseInt(process.env.PORT, 10) + 2 || 3002,
39 | proxy: `0.0.0.0:${parseInt(process.env.PORT, 10) || 3000}`
40 | });
41 |
42 | // Listen for `rs` in stdin to restart server
43 | debug('dev')('type `rs` in console for restarting koa application');
44 | process.stdin.setEncoding('utf8');
45 | process.stdin.on('data', function(data) {
46 | const parsedData = (data + '').trim().toLowerCase();
47 | if (parsedData === 'rs') return restartServer();
48 | });
49 |
50 | // Start watcher on server files
51 | // and reload browser on change
52 | watch(
53 | path.join(__dirname, '../../server'),
54 | (file) => !file.match('webpack-stats.json') ? restartServer() : noop()
55 | );
56 | }
57 | }
58 | });
59 | };
60 |
61 | // kill server on exit
62 | process.on('exit', () => server.kill('SIGTERM'));
63 | export default () => !server ? startServer() : noop();
64 |
--------------------------------------------------------------------------------
/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))) {
18 | chunks = [chunks];
19 | }
20 |
21 | return chunks
22 | .filter(chunk => ext.test(path.extname(chunk))) // filter by extension
23 | .map(chunk => `${publicPath}${chunk}`); // add public path to it
24 | };
25 |
26 | const script = getChunks('app', /js/);
27 | const style = getChunks('app', /css/);
28 |
29 | // Find compiled images in modules
30 | // it will be used to map original filename to the compiled one
31 | // for server side rendering
32 | const imagesRegex = /\.(jpe?g|png|gif|svg)$/;
33 | const images = json.modules
34 | .filter(module => imagesRegex.test(module.name))
35 | .map(image => {
36 | return {
37 | original: image.name,
38 | compiled: `${publicPath}${image.assets[0]}`
39 | };
40 | });
41 |
42 | const content = { script, style, images };
43 |
44 | fs.writeFileSync(filepath, JSON.stringify(content));
45 | debug('dev')('`webpack-stats.json` updated');
46 | }
47 |
--------------------------------------------------------------------------------