` also works. (e.g. `npm run generate container`)
19 |
20 | ### Learn more
21 |
22 | - [Redux](redux.md)
23 | - [ImmutableJS](immutablejs.md)
24 | - [reselect](reselect.md)
25 | - [redux-saga](redux-saga.md)
26 | - [react-intl](i18n.md)
27 | - [routing](routing.md)
28 | - [Asynchronously loaded components](async-components.md)
29 |
30 | ## Architecture: `components` and `containers`
31 |
32 | We adopted a split between stateless, reusable components called (wait for it...)
33 | `components` and stateful parent components called `containers`.
34 |
35 | ### Learn more
36 |
37 | See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
38 | by Dan Abramov for a great introduction to this approach.
39 |
--------------------------------------------------------------------------------
/frontend/docs/js/async-components.md:
--------------------------------------------------------------------------------
1 | # Loading components with react-loadable
2 |
3 | [`react-loadable`](https://github.com/thejameskyle/react-loadable) is integrated into
4 | `react-boilerplate` because of its rich functionality and good design (it does not
5 | conflate routes with asynchronous components).
6 |
7 | To load component asynchronously, create a `Loadable` file by hand or via component/container generators with
8 | 'Do you want an async loader?' option activated. This is how it can look like:
9 |
10 | ```JS
11 | import Loadable from 'react-loadable';
12 |
13 | import LoadingIndicator from 'components/LoadingIndicator';
14 |
15 | export default Loadable({
16 | loader: () => import('./index'),
17 | loading: LoadingIndicator,
18 | });
19 | ```
20 |
21 | You can find more information on how to use `react-loadable` in [their docs](https://github.com/thejameskyle/react-loadable).
22 |
--------------------------------------------------------------------------------
/frontend/docs/js/reselect.md:
--------------------------------------------------------------------------------
1 | # `reselect`
2 |
3 | reselect memoizes ("caches") previous state trees and calculations based on said
4 | tree. This means repeated changes and calculations are fast and efficient,
5 | providing us with a performance boost over standard `mapStateToProps`
6 | implementations.
7 |
8 | The [official documentation](https://github.com/reactjs/reselect)
9 | offers a good starting point!
10 |
11 | ## Usage
12 |
13 | There are two different kinds of selectors, simple and complex ones.
14 |
15 | ### Simple selectors
16 |
17 | Simple selectors are just that: they take the application state and select a
18 | part of it.
19 |
20 | ```javascript
21 | const mySelector = state => state.get('someState');
22 |
23 | export { mySelector };
24 | ```
25 |
26 | ### Complex selectors
27 |
28 | If we need to, we can combine simple selectors to build more complex ones which
29 | get nested state parts with reselect's `createSelector` function. We import other
30 | selectors and pass them to the `createSelector` call:
31 |
32 | ```javascript
33 | import { createSelector } from 'reselect';
34 | import mySelector from 'mySelector';
35 |
36 | const myComplexSelector = createSelector(mySelector, myState =>
37 | myState.get('someNestedState')
38 | );
39 |
40 | export { myComplexSelector };
41 | ```
42 |
43 | These selectors can then either be used directly in our containers as
44 | `mapStateToProps` functions or be nested with `createSelector` once again:
45 |
46 | ```javascript
47 | export default connect(
48 | createSelector(myComplexSelector, myNestedState => ({ data: myNestedState }))
49 | )(SomeComponent);
50 | ```
51 |
52 | ### Adding a new selector
53 |
54 | If you have a `selectors.js` file next to the reducer which's part of the state
55 | you want to select, add your selector to said file. If you don't have one yet,
56 | add a new one into your container folder and fill it with this boilerplate code:
57 |
58 | ```JS
59 | import { createSelector } from 'reselect';
60 |
61 | const selectMyState = () => createSelector(
62 |
63 | );
64 |
65 | export {
66 | selectMyState,
67 | };
68 | ```
69 |
70 | ---
71 |
72 | _Don't like this feature? [Click here](remove.md)_
73 |
--------------------------------------------------------------------------------
/frontend/docs/js/routing.md:
--------------------------------------------------------------------------------
1 | # Routing via `react-router` and `react-router-redux`
2 |
3 | `react-router` is the de-facto standard routing solution for react applications.
4 | The thing is that with redux and a single state tree, the URL is part of that
5 | state. `react-router-redux` takes care of synchronizing the location of our
6 | application with the application state.
7 |
8 | (See the [`react-router-redux` documentation](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux)
9 | for more information)
10 |
11 | ## Usage
12 |
13 | To add a new route, simply import the `Route` component and use it standalone or inside the `Switch` component (all part of [RR4 API](https://reacttraining.com/react-router/web/api)):
14 |
15 | ```JS
16 |
17 | ```
18 |
19 | Top level routes are located in `App.js`.
20 |
21 | If you want your route component (or any component for that matter) to be loaded asynchronously, use container or component generator with 'Do you want an async loader?' option activated.
22 |
23 | To go to a new page use the `push` function by `react-router-redux`:
24 |
25 | ```JS
26 | import { push } from 'react-router-redux';
27 |
28 | dispatch(push('/some/page'));
29 | ```
30 |
31 | ## Child Routes
32 |
33 | For example, if you have a route called `about` at `/about` and want to make a child route called `team` at `/about/our-team`, follow the example
34 | in `App.js` to create a `Switch` within the parent component. Also remove the `exact` property from the `about` parent route.
35 |
36 | ```JS
37 | // AboutPage/index.js
38 | import { Switch, Route } from 'react-router-dom';
39 |
40 | class AboutPage extends React.PureComponent {
41 | render() {
42 | return (
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 | ```
50 |
51 | Note that with React Router v4, route re-rendering is handled by React's `setState`. This
52 | means that when wrapping route components in a redux connected container, or `PureComponent` or any other component with
53 | `shouldComponentUpdate`, you need to create a [ConnectedSwitch](https://github.com/ReactTraining/react-router/issues/5072#issuecomment-310184271)
54 | container that receives `location` directly from a redux store. Read more about this in
55 | [Dealing with Update Blocking](https://reacttraining.com/react-router/web/guides/dealing-with-update-blocking).
56 |
57 | You can read more in [`react-router`'s documentation](https://reacttraining.com/react-router/web/api).
58 |
--------------------------------------------------------------------------------
/frontend/docs/testing/README.md:
--------------------------------------------------------------------------------
1 | # Testing
2 |
3 | - [Unit Testing](unit-testing.md)
4 | - [Component Testing](component-testing.md)
5 | - [Remote Testing](remote-testing.md)
6 |
7 | Testing your application is a vital part of serious development. There are a few
8 | things you should test. If you've never done this before start with [unit testing](unit-testing.md).
9 | Move on to [component testing](component-testing.md) when you feel like you
10 | understand that!
11 |
12 | We also support [remote testing](remote-testing.md) your local application,
13 | which is quite awesome, so definitely check that out!
14 |
15 | ## Usage with this boilerplate
16 |
17 | To test your application started with this boilerplate do the following:
18 |
19 | 1. Sprinkle `.test.js` files directly next to the parts of your application you
20 | want to test. (Or in `test/` subdirectories, it doesn't really matter as long
21 | as they are directly next to those parts and end in `.test.js`)
22 |
23 | 1. Write your unit and component tests in those files.
24 |
25 | 1. Run `npm run test` in your terminal and see all the tests pass! (hopefully)
26 |
27 | There are a few more commands related to testing, checkout the [commands documentation](../general/commands.md#testing)
28 | for the full list!
29 |
--------------------------------------------------------------------------------
/frontend/docs/testing/remote-testing.md:
--------------------------------------------------------------------------------
1 | # Remote testing
2 |
3 | ```Shell
4 | npm run start:tunnel
5 | ```
6 |
7 | This command will start a server and tunnel it with `ngrok`. You'll get a URL
8 | that looks a bit like this: `http://abcdef.ngrok.com`
9 |
10 | This URL will show the version of your application that's in the `build` folder,
11 | and it's accessible from the entire world! This is great for testing on different
12 | devices and from different locations!
13 |
--------------------------------------------------------------------------------
/frontend/internals/config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const pullAll = require('lodash/pullAll');
3 | const uniq = require('lodash/uniq');
4 |
5 | const ReactBoilerplate = {
6 | // This refers to the react-boilerplate version this project is based on.
7 | version: '3.6.0',
8 |
9 | /**
10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading
11 | * by caching the module metadata for all of our npm dependencies. We enable it by default
12 | * in development.
13 | *
14 | *
15 | * To disable the DLL Plugin, set this value to false.
16 | */
17 | dllPlugin: {
18 | defaults: {
19 | /**
20 | * we need to exclude dependencies which are not intended for the browser
21 | * by listing them here.
22 | */
23 | exclude: [
24 | 'chalk',
25 | 'compression',
26 | 'cross-env',
27 | 'express',
28 | 'ip',
29 | 'minimist',
30 | 'sanitize.css',
31 | ],
32 |
33 | /**
34 | * Specify any additional dependencies here. We include core-js and lodash
35 | * since a lot of our dependencies depend on them and they get picked up by webpack.
36 | */
37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'],
38 |
39 | // The path where the DLL manifest and bundle will get built
40 | path: resolve('../node_modules/react-boilerplate-dlls'),
41 | },
42 |
43 | entry(pkg) {
44 | const dependencyNames = Object.keys(pkg.dependencies);
45 | const exclude =
46 | pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude;
47 | const include =
48 | pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include;
49 | const includeDependencies = uniq(dependencyNames.concat(include));
50 |
51 | return {
52 | reactBoilerplateDeps: pullAll(includeDependencies, exclude),
53 | };
54 | },
55 | },
56 | };
57 |
58 | module.exports = ReactBoilerplate;
59 |
--------------------------------------------------------------------------------
/frontend/internals/generators/component/class.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{ properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | // import PropTypes from 'prop-types';
9 | // import styled from 'styled-components';
10 |
11 | {{#if wantMessages}}
12 | import { FormattedMessage } from 'react-intl';
13 | import messages from './messages';
14 | {{/if}}
15 |
16 | /* eslint-disable react/prefer-stateless-function */
17 | class {{ properCase name }} extends {{{ type }}} {
18 | render() {
19 | return (
20 |
21 | {{#if wantMessages}}
22 |
23 | {{/if}}
24 |
25 | );
26 | }
27 | }
28 |
29 | {{ properCase name }}.propTypes = {};
30 |
31 | export default {{ properCase name }};
32 |
--------------------------------------------------------------------------------
/frontend/internals/generators/component/loadable.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Asynchronously loads the component for {{ properCase name }}
4 | *
5 | */
6 |
7 | import Loadable from 'react-loadable';
8 |
9 | export default Loadable({
10 | loader: () => import('./index'),
11 | loading: () => null,
12 | });
13 |
--------------------------------------------------------------------------------
/frontend/internals/generators/component/messages.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | * {{ properCase name }} Messages
3 | *
4 | * This contains all the text for the {{ properCase name }} component.
5 | */
6 |
7 | import { defineMessages } from 'react-intl';
8 |
9 | export default defineMessages({
10 | header: {
11 | id: 'app.components.{{ properCase name }}.header',
12 | defaultMessage: 'This is the {{ properCase name}} component !',
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/internals/generators/component/stateless.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{ properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | // import PropTypes from 'prop-types';
9 | // import styled from 'styled-components';
10 |
11 | {{#if wantMessages}}
12 | import { FormattedMessage } from 'react-intl';
13 | import messages from './messages';
14 | {{/if}}
15 |
16 | function {{ properCase name }}() {
17 | return (
18 |
19 | {{#if wantMessages}}
20 |
21 | {{/if}}
22 |
23 | );
24 | }
25 |
26 | {{ properCase name }}.propTypes = {};
27 |
28 | export default {{ properCase name }};
29 |
--------------------------------------------------------------------------------
/frontend/internals/generators/component/test.js.hbs:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import {{ properCase name }} from '../index';
5 |
6 | describe('<{{ properCase name }} />', () => {
7 | it('Expect to have unit tests specified', () => {
8 | expect(true).toEqual(false);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/actions.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} actions
4 | *
5 | */
6 |
7 | import { DEFAULT_ACTION } from './constants';
8 |
9 | export function defaultAction() {
10 | return {
11 | type: DEFAULT_ACTION,
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/actions.test.js.hbs:
--------------------------------------------------------------------------------
1 | import { defaultAction } from '../actions';
2 | import { DEFAULT_ACTION } from '../constants';
3 |
4 | describe('{{ properCase name }} actions', () => {
5 | describe('Default Action', () => {
6 | it('has a type of DEFAULT_ACTION', () => {
7 | const expected = {
8 | type: DEFAULT_ACTION,
9 | };
10 | expect(defaultAction()).toEqual(expected);
11 | });
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/class.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | {{/if}}
19 | import { compose } from 'redux';
20 |
21 | {{#if wantSaga}}
22 | import injectSaga from 'utils/injectSaga';
23 | {{/if}}
24 | {{#if wantActionsAndReducer}}
25 | import injectReducer from 'utils/injectReducer';
26 | import makeSelect{{properCase name}} from './selectors';
27 | import reducer from './reducer';
28 | {{/if}}
29 | {{#if wantSaga}}
30 | import saga from './saga';
31 | {{/if}}
32 | {{#if wantMessages}}
33 | import messages from './messages';
34 | {{/if}}
35 |
36 | /* eslint-disable react/prefer-stateless-function */
37 | export class {{ properCase name }} extends {{{ type }}} {
38 | render() {
39 | return (
40 |
41 | {{#if wantHeaders}}
42 |
43 | {{properCase name}}
44 |
45 |
46 | {{/if}}
47 | {{#if wantMessages}}
48 |
49 | {{/if}}
50 |
51 | );
52 | }
53 | }
54 |
55 | {{ properCase name }}.propTypes = {
56 | dispatch: PropTypes.func.isRequired,
57 | };
58 |
59 | {{#if wantActionsAndReducer}}
60 | const mapStateToProps = createStructuredSelector({
61 | {{ lowerCase name }}: makeSelect{{properCase name}}(),
62 | });
63 | {{/if}}
64 |
65 | function mapDispatchToProps(dispatch) {
66 | return {
67 | dispatch,
68 | };
69 | }
70 |
71 | {{#if wantActionsAndReducer}}
72 | const withConnect = connect(
73 | mapStateToProps,
74 | mapDispatchToProps
75 | );
76 |
77 | const withReducer = injectReducer({ key: '{{ camelCase name }}', reducer });
78 | {{else}}
79 | const withConnect = connect(null, mapDispatchToProps);
80 | {{/if}}
81 | {{#if wantSaga}}
82 | const withSaga = injectSaga({ key: '{{ camelCase name }}', saga });
83 | {{/if}}
84 |
85 | export default compose(
86 | {{#if wantActionsAndReducer}}
87 | withReducer,
88 | {{/if}}
89 | {{#if wantSaga}}
90 | withSaga,
91 | {{/if}}
92 | withConnect
93 | )({{ properCase name }});
94 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/constants.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} constants
4 | *
5 | */
6 |
7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION';
8 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/index.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | import makeSelect{{properCase name}} from './selectors';
19 | {{/if}}
20 | {{#if wantMessages}}
21 | import messages from './messages';
22 | {{/if}}
23 |
24 | /* eslint-disable react/prefer-stateless-function */
25 | export class {{ properCase name }} extends React.{{{ component }}} {
26 | render() {
27 | return (
28 |
29 | {{#if wantHeaders}}
30 |
31 | {{properCase name}}
32 |
36 |
37 | {{/if}}
38 | {{#if wantMessages}}
39 |
40 | {{/if}}
41 |
42 | );
43 | }
44 | }
45 |
46 | {{ properCase name }}.propTypes = {
47 | dispatch: PropTypes.func.isRequired,
48 | };
49 |
50 | {{#if wantActionsAndReducer}}
51 | const mapStateToProps = createStructuredSelector({
52 | {{name}}: makeSelect{{properCase name}}(),
53 | });
54 | {{/if}}
55 |
56 | function mapDispatchToProps(dispatch) {
57 | return {
58 | dispatch,
59 | };
60 | }
61 |
62 | {{#if wantActionsAndReducer}}
63 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }});
64 | {{else}}
65 | export default connect(null, mapDispatchToProps)({{ properCase name }});
66 | {{/if}}
67 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/messages.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | * {{properCase name }} Messages
3 | *
4 | * This contains all the text for the {{properCase name }} component.
5 | */
6 |
7 | import { defineMessages } from 'react-intl';
8 |
9 | export default defineMessages({
10 | header: {
11 | id: 'app.containers.{{properCase name }}.header',
12 | defaultMessage: 'This is {{properCase name}} container !',
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/reducer.js.hbs:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * {{ properCase name }} reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 | import { DEFAULT_ACTION } from './constants';
9 |
10 | export const initialState = fromJS({});
11 |
12 | function {{ camelCase name }}Reducer(state = initialState, action) {
13 | switch (action.type) {
14 | case DEFAULT_ACTION:
15 | return state;
16 | default:
17 | return state;
18 | }
19 | }
20 |
21 | export default {{ camelCase name }}Reducer;
22 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/reducer.test.js.hbs:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 | import {{ camelCase name }}Reducer from '../reducer';
3 |
4 | describe('{{ camelCase name }}Reducer', () => {
5 | it('returns the initial state', () => {
6 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({}));
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/saga.js.hbs:
--------------------------------------------------------------------------------
1 | // import { take, call, put, select } from 'redux-saga/effects';
2 |
3 | // Individual exports for testing
4 | export default function* defaultSaga() {
5 | // See example in containers/HomePage/saga.js
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/saga.test.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | * Test sagas
3 | */
4 |
5 | /* eslint-disable redux-saga/yield-effects */
6 | // import { take, call, put, select } from 'redux-saga/effects';
7 | // import { defaultSaga } from '../saga';
8 |
9 | // const generator = defaultSaga();
10 |
11 | describe('defaultSaga Saga', () => {
12 | it('Expect to have unit tests specified', () => {
13 | expect(true).toEqual(false);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/selectors.js.hbs:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { initialState } from './reducer';
3 |
4 | /**
5 | * Direct selector to the {{ camelCase name }} state domain
6 | */
7 |
8 | const select{{ properCase name }}Domain = state =>
9 | state.get('{{ camelCase name }}', initialState);
10 |
11 | /**
12 | * Other specific selectors
13 | */
14 |
15 | /**
16 | * Default selector used by {{ properCase name }}
17 | */
18 |
19 | const makeSelect{{ properCase name }} = () =>
20 | createSelector(select{{ properCase name }}Domain, substate => substate.toJS());
21 |
22 | export default makeSelect{{ properCase name }};
23 | export { select{{ properCase name }}Domain };
24 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/selectors.test.js.hbs:
--------------------------------------------------------------------------------
1 | // import { fromJS } from 'immutable';
2 | // import { select{{ properCase name }}Domain } from '../selectors';
3 |
4 | describe('select{{ properCase name }}Domain', () => {
5 | it('Expect to have unit tests specified', () => {
6 | expect(true).toEqual(false);
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/stateless.js.hbs:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * {{properCase name }}
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import PropTypes from 'prop-types';
9 | import { connect } from 'react-redux';
10 | {{#if wantHeaders}}
11 | import { Helmet } from 'react-helmet';
12 | {{/if}}
13 | {{#if wantMessages}}
14 | import { FormattedMessage } from 'react-intl';
15 | {{/if}}
16 | {{#if wantActionsAndReducer}}
17 | import { createStructuredSelector } from 'reselect';
18 | {{/if}}
19 | import { compose } from 'redux';
20 |
21 | {{#if wantSaga}}
22 | import injectSaga from 'utils/injectSaga';
23 | {{/if}}
24 | {{#if wantActionsAndReducer}}
25 | import injectReducer from 'utils/injectReducer';
26 | import makeSelect{{properCase name}} from './selectors';
27 | import reducer from './reducer';
28 | {{/if}}
29 | {{#if wantSaga}}
30 | import saga from './saga';
31 | {{/if}}
32 | {{#if wantMessages}}
33 | import messages from './messages';
34 | {{/if}}
35 |
36 | function {{ properCase name }}() {
37 | return (
38 |
39 | {{#if wantHeaders}}
40 |
41 | {{properCase name}}
42 |
43 |
44 | {{/if}}
45 | {{#if wantMessages}}
46 |
47 | {{/if}}
48 |
49 | );
50 | }
51 |
52 | {{ properCase name }}.propTypes = {
53 | dispatch: PropTypes.func.isRequired,
54 | };
55 |
56 | {{#if wantActionsAndReducer}}
57 | const mapStateToProps = createStructuredSelector({
58 | {{ lowerCase name }}: makeSelect{{properCase name}}(),
59 | });
60 | {{/if}}
61 |
62 | function mapDispatchToProps(dispatch) {
63 | return {
64 | dispatch,
65 | };
66 | }
67 |
68 | {{#if wantActionsAndReducer}}
69 | const withConnect = connect(mapStateToProps, mapDispatchToProps);
70 |
71 | const withReducer = injectReducer({ key: '{{ lowerCase name }}', reducer });
72 | {{else}}
73 | const withConnect = connect(null, mapDispatchToProps);
74 | {{/if}}
75 | {{#if wantSaga}}
76 | const withSaga = injectSaga({ key: '{{ lowerCase name }}', saga });
77 | {{/if}}
78 |
79 | export default compose(
80 | {{#if wantActionsAndReducer}}
81 | withReducer,
82 | {{/if}}
83 | {{#if wantSaga}}
84 | withSaga,
85 | {{/if}}
86 | withConnect,
87 | )({{ properCase name }});
88 |
--------------------------------------------------------------------------------
/frontend/internals/generators/container/test.js.hbs:
--------------------------------------------------------------------------------
1 | // import React from 'react';
2 | // import { shallow } from 'enzyme';
3 |
4 | // import { {{ properCase name }} } from '../index';
5 |
6 | describe('<{{ properCase name }} />', () => {
7 | it('Expect to have unit tests specified', () => {
8 | expect(true).toEqual(false);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/frontend/internals/generators/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * generator/index.js
3 | *
4 | * Exports the generators so plop knows them
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const { exec } = require('child_process');
10 | const componentGenerator = require('./component/index.js');
11 | const containerGenerator = require('./container/index.js');
12 | const languageGenerator = require('./language/index.js');
13 |
14 | module.exports = plop => {
15 | plop.setGenerator('component', componentGenerator);
16 | plop.setGenerator('container', containerGenerator);
17 | plop.setGenerator('language', languageGenerator);
18 | plop.addHelper('directory', comp => {
19 | try {
20 | fs.accessSync(
21 | path.join(__dirname, `../../app/containers/${comp}`),
22 | fs.F_OK,
23 | );
24 | return `containers/${comp}`;
25 | } catch (e) {
26 | return `components/${comp}`;
27 | }
28 | });
29 | plop.addHelper('curly', (object, open) => (open ? '{' : '}'));
30 | plop.setActionType('prettify', (answers, config) => {
31 | const folderPath = `${path.join(
32 | __dirname,
33 | '/../../app/',
34 | config.path,
35 | plop.getHelper('properCase')(answers.name),
36 | '**.js',
37 | )}`;
38 | exec(`npm run prettify -- "${folderPath}"`);
39 | return folderPath;
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/add-locale-data.hbs:
--------------------------------------------------------------------------------
1 | $1addLocaleData({{language}}LocaleData);
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/app-locale.hbs:
--------------------------------------------------------------------------------
1 | $1 '{{language}}',
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/format-translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages),
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/intl-locale-data.hbs:
--------------------------------------------------------------------------------
1 | $&const {{language}}LocaleData = require('react-intl/locale-data/{{language}}');
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/polyfill-intl-locale.hbs:
--------------------------------------------------------------------------------
1 | $1 import('intl/locale-data/jsonp/{{language}}.js'),
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/translation-messages.hbs:
--------------------------------------------------------------------------------
1 | $1const {{language}}TranslationMessages = require('./translations/{{language}}.json');
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/language/translations-json.hbs:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/frontend/internals/generators/utils/componentExists.js:
--------------------------------------------------------------------------------
1 | /**
2 | * componentExists
3 | *
4 | * Check whether the given component exist in either the components or containers directory
5 | */
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const pageComponents = fs.readdirSync(
10 | path.join(__dirname, '../../../app/components'),
11 | );
12 | const pageContainers = fs.readdirSync(
13 | path.join(__dirname, '../../../app/containers'),
14 | );
15 | const components = pageComponents.concat(pageContainers);
16 |
17 | function componentExists(comp) {
18 | return components.indexOf(comp) >= 0;
19 | }
20 |
21 | module.exports = componentExists;
22 |
--------------------------------------------------------------------------------
/frontend/internals/mocks/cssModule.js:
--------------------------------------------------------------------------------
1 | module.exports = 'CSS_MODULE';
2 |
--------------------------------------------------------------------------------
/frontend/internals/mocks/image.js:
--------------------------------------------------------------------------------
1 | module.exports = 'IMAGE_MOCK';
2 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/analyze.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const shelljs = require('shelljs');
4 | const animateProgress = require('./helpers/progress');
5 | const chalk = require('chalk');
6 | const addCheckMark = require('./helpers/checkmark');
7 |
8 | const progress = animateProgress('Generating stats');
9 |
10 | // Generate stats.json file with webpack
11 | shelljs.exec(
12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json',
13 | addCheckMark.bind(null, callback), // Output a checkmark on completion
14 | );
15 |
16 | // Called after webpack has finished generating the stats.json file
17 | function callback() {
18 | clearInterval(progress);
19 | process.stdout.write(
20 | '\n\nOpen ' +
21 | chalk.magenta('http://webpack.github.io/analyse/') +
22 | ' in your browser and upload the stats.json file!' +
23 | chalk.blue(
24 | '\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n',
25 | ),
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/clean.js:
--------------------------------------------------------------------------------
1 | const shell = require('shelljs');
2 | const addCheckMark = require('./helpers/checkmark.js');
3 |
4 | if (!shell.which('git')) {
5 | shell.echo('Sorry, this script requires git');
6 | shell.exit(1);
7 | }
8 |
9 | if (!shell.test('-e', 'internals/templates')) {
10 | shell.echo('The example is deleted already.');
11 | shell.exit(1);
12 | }
13 |
14 | process.stdout.write('Cleanup started...');
15 |
16 | // Reuse existing LanguageProvider and i18n tests
17 | shell.mv(
18 | 'app/containers/LanguageProvider/tests',
19 | 'internals/templates/containers/LanguageProvider',
20 | );
21 | shell.cp('app/tests/i18n.test.js', 'internals/templates/tests/i18n.test.js');
22 |
23 | // Cleanup components/
24 | shell.rm('-rf', 'app/components/*');
25 |
26 | // Handle containers/
27 | shell.rm('-rf', 'app/containers');
28 | shell.mv('internals/templates/containers', 'app');
29 |
30 | // Handle tests/
31 | shell.mv('internals/templates/tests', 'app');
32 |
33 | // Handle translations/
34 | shell.rm('-rf', 'app/translations');
35 | shell.mv('internals/templates/translations', 'app');
36 |
37 | // Handle utils/
38 | shell.rm('-rf', 'app/utils');
39 | shell.mv('internals/templates/utils', 'app');
40 |
41 | // Replace the files in the root app/ folder
42 | shell.cp('internals/templates/app.js', 'app/app.js');
43 | shell.cp('internals/templates/global-styles.js', 'app/global-styles.js');
44 | shell.cp('internals/templates/i18n.js', 'app/i18n.js');
45 | shell.cp('internals/templates/index.html', 'app/index.html');
46 | shell.cp('internals/templates/reducers.js', 'app/reducers.js');
47 | shell.cp('internals/templates/configureStore.js', 'app/configureStore.js');
48 |
49 | // Remove the templates folder
50 | shell.rm('-rf', 'internals/templates');
51 |
52 | addCheckMark();
53 |
54 | // Commit the changes
55 | if (
56 | shell.exec('git add . --all && git commit -qm "Remove default example"')
57 | .code !== 0
58 | ) {
59 | shell.echo('\nError: Git commit failed');
60 | shell.exit(1);
61 | }
62 |
63 | shell.echo('\nCleanup done. Happy Coding!!!');
64 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/dependencies.js:
--------------------------------------------------------------------------------
1 | // No need to build the DLL in production
2 | if (process.env.NODE_ENV === 'production') {
3 | process.exit(0);
4 | }
5 |
6 | require('shelljs/global');
7 |
8 | const path = require('path');
9 | const fs = require('fs');
10 | const exists = fs.existsSync;
11 | const writeFile = fs.writeFileSync;
12 |
13 | const defaults = require('lodash/defaultsDeep');
14 | const pkg = require(path.join(process.cwd(), 'package.json'));
15 | const config = require('../config');
16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults);
17 | const outputPath = path.join(process.cwd(), dllConfig.path);
18 | const dllManifestPath = path.join(outputPath, 'package.json');
19 |
20 | /**
21 | * I use node_modules/react-boilerplate-dlls by default just because
22 | * it isn't going to be version controlled and babel wont try to parse it.
23 | */
24 | mkdir('-p', outputPath);
25 |
26 | echo('Building the Webpack DLL...');
27 |
28 | /**
29 | * Create a manifest so npm install doesn't warn us
30 | */
31 | if (!exists(dllManifestPath)) {
32 | writeFile(
33 | dllManifestPath,
34 | JSON.stringify(
35 | defaults({
36 | name: 'react-boilerplate-dlls',
37 | private: true,
38 | author: pkg.author,
39 | repository: pkg.repository,
40 | version: pkg.version,
41 | }),
42 | null,
43 | 2,
44 | ),
45 | 'utf8',
46 | );
47 | }
48 |
49 | // the BUILDING_DLL env var is set to avoid confusing the development environment
50 | exec(
51 | 'cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js --hide-modules',
52 | );
53 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/helpers/checkmark.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | /**
4 | * Adds mark check symbol
5 | */
6 | function addCheckMark(callback) {
7 | process.stdout.write(chalk.green(' ✓'));
8 | if (callback) callback();
9 | }
10 |
11 | module.exports = addCheckMark;
12 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/helpers/progress.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const readline = require('readline');
4 |
5 | /**
6 | * Adds an animated progress indicator
7 | *
8 | * @param {string} message The message to write next to the indicator
9 | * @param {number} amountOfDots The amount of dots you want to animate
10 | */
11 | function animateProgress(message, amountOfDots) {
12 | if (typeof amountOfDots !== 'number') {
13 | amountOfDots = 3;
14 | }
15 |
16 | let i = 0;
17 | return setInterval(function() {
18 | readline.cursorTo(process.stdout, 0);
19 | i = (i + 1) % (amountOfDots + 1);
20 | const dots = new Array(i + 1).join('.');
21 | process.stdout.write(message + dots);
22 | }, 500);
23 | }
24 |
25 | module.exports = animateProgress;
26 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/helpers/xmark.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 |
3 | /**
4 | * Adds mark cross symbol
5 | */
6 | function addXMark(callback) {
7 | process.stdout.write(chalk.red(' ✘'));
8 | if (callback) callback();
9 | }
10 |
11 | module.exports = addXMark;
12 |
--------------------------------------------------------------------------------
/frontend/internals/scripts/npmcheckversion.js:
--------------------------------------------------------------------------------
1 | const exec = require('child_process').exec;
2 | exec('npm -v', function(err, stdout, stderr) {
3 | if (err) throw err;
4 | if (parseFloat(stdout) < 3) {
5 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3');
6 | process.exit(1);
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/internals/testing/enzyme-setup.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/frontend/internals/testing/test-bundler.js:
--------------------------------------------------------------------------------
1 | // needed for regenerator-runtime
2 | // (ES7 generator support is required by redux-saga)
3 | import 'babel-polyfill';
4 |
--------------------------------------------------------------------------------
/frontend/internals/webpack/webpack.dll.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WEBPACK DLL GENERATOR
3 | *
4 | * This profile is used to cache webpack's module
5 | * contexts for external library and framework type
6 | * dependencies which will usually not change often enough
7 | * to warrant building them from scratch every time we use
8 | * the webpack process.
9 | */
10 |
11 | const { join } = require('path');
12 | const defaults = require('lodash/defaultsDeep');
13 | const webpack = require('webpack');
14 | const pkg = require(join(process.cwd(), 'package.json'));
15 | const { dllPlugin } = require('../config');
16 |
17 | if (!pkg.dllPlugin) {
18 | process.exit(0);
19 | }
20 |
21 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults);
22 | const outputPath = join(process.cwd(), dllConfig.path);
23 |
24 | module.exports = require('./webpack.base.babel')({
25 | mode: 'development',
26 | context: process.cwd(),
27 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg),
28 | optimization: {
29 | minimize: false,
30 | },
31 | devtool: 'eval',
32 | output: {
33 | filename: '[name].dll.js',
34 | path: outputPath,
35 | library: '[name]',
36 | },
37 | plugins: [
38 | new webpack.DllPlugin({
39 | name: '[name]',
40 | path: join(outputPath, '[name].json'),
41 | }),
42 | ],
43 | performance: {
44 | hints: false,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/frontend/server/argv.js:
--------------------------------------------------------------------------------
1 | module.exports = require('minimist')(process.argv.slice(2));
2 |
--------------------------------------------------------------------------------
/frontend/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint consistent-return:0 */
2 |
3 | const express = require('express');
4 | const logger = require('./logger');
5 | const proxy = require('http-proxy-middleware');
6 |
7 | const argv = require('./argv');
8 | const port = require('./port');
9 | const setup = require('./middlewares/frontendMiddleware');
10 | const isDev = process.env.NODE_ENV !== 'production';
11 | const ngrok =
12 | (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel
13 | ? require('ngrok')
14 | : false;
15 | const { resolve } = require('path');
16 | const app = express();
17 |
18 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here
19 | const myApi = proxy('/api', { target: 'http://localhost:8000'})
20 | app.use('/api', myApi);
21 | app.use('/media', proxy('/media', { target: 'http://localhost:8000'}));
22 |
23 |
24 | // In production we need to pass these values in instead of relying on webpack
25 | setup(app, {
26 | outputPath: resolve(process.cwd(), 'build'),
27 | publicPath: '/',
28 | });
29 |
30 | // get the intended host and port number, use localhost and port 3000 if not provided
31 | const customHost = argv.host || process.env.HOST;
32 | const host = customHost || null; // Let http.Server use its default IPv6/4 host
33 | const prettyHost = customHost || 'localhost';
34 |
35 | // Start your app.
36 | app.listen(port, host, async err => {
37 | if (err) {
38 | return logger.error(err.message);
39 | }
40 |
41 | // Connect to ngrok in dev mode
42 | if (ngrok) {
43 | let url;
44 | try {
45 | url = await ngrok.connect(port);
46 | } catch (e) {
47 | return logger.error(e);
48 | }
49 | logger.appStarted(port, prettyHost, url);
50 | } else {
51 | logger.appStarted(port, prettyHost);
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/frontend/server/logger.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const chalk = require('chalk');
4 | const ip = require('ip');
5 |
6 | const divider = chalk.gray('\n-----------------------------------');
7 |
8 | /**
9 | * Logger middleware, you can customize it to make messages more personal
10 | */
11 | const logger = {
12 | // Called whenever there's an error on the server we want to print
13 | error: err => {
14 | console.error(chalk.red(err));
15 | },
16 |
17 | // Called when express.js app starts on given port w/o errors
18 | appStarted: (port, host, tunnelStarted) => {
19 | console.log(`Server started ! ${chalk.green('✓')}`);
20 |
21 | // If the tunnel started, log that and the URL it's available at
22 | if (tunnelStarted) {
23 | console.log(`Tunnel initialised ${chalk.green('✓')}`);
24 | }
25 |
26 | console.log(`
27 | ${chalk.bold('Access URLs:')}${divider}
28 | Localhost: ${chalk.magenta(`http://${host}:${port}`)}
29 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) +
30 | (tunnelStarted
31 | ? `\n Proxy: ${chalk.magenta(tunnelStarted)}`
32 | : '')}${divider}
33 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)}
34 | `);
35 | },
36 | };
37 |
38 | module.exports = logger;
39 |
--------------------------------------------------------------------------------
/frontend/server/middlewares/addDevMiddlewares.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const webpackDevMiddleware = require('webpack-dev-middleware');
4 | const webpackHotMiddleware = require('webpack-hot-middleware');
5 |
6 | function createWebpackMiddleware(compiler, publicPath) {
7 | return webpackDevMiddleware(compiler, {
8 | logLevel: 'warn',
9 | publicPath,
10 | silent: true,
11 | stats: 'errors-only',
12 | });
13 | }
14 |
15 | module.exports = function addDevMiddlewares(app, webpackConfig) {
16 | const compiler = webpack(webpackConfig);
17 | const middleware = createWebpackMiddleware(
18 | compiler,
19 | webpackConfig.output.publicPath,
20 | );
21 |
22 | app.use(middleware);
23 | app.use(webpackHotMiddleware(compiler));
24 |
25 | // Since webpackDevMiddleware uses memory-fs internally to store build
26 | // artifacts, we use it instead
27 | const fs = middleware.fileSystem;
28 |
29 | app.get('*', (req, res) => {
30 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
31 | if (err) {
32 | res.sendStatus(404);
33 | } else {
34 | res.send(file.toString());
35 | }
36 | });
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/server/middlewares/addProdMiddlewares.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const compression = require('compression');
4 |
5 | module.exports = function addProdMiddlewares(app, options) {
6 | const publicPath = options.publicPath || '/';
7 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
8 |
9 | // compression middleware compresses your server responses which makes them
10 | // smaller (applies also to assets). You can read more about that technique
11 | // and other good practices on official Express.js docs http://mxs.is/googmy
12 | app.use(compression());
13 | app.use(publicPath, express.static(outputPath));
14 |
15 | app.get('*', (req, res) =>
16 | res.sendFile(path.resolve(outputPath, 'index.html')),
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/frontend/server/middlewares/frontendMiddleware.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | /**
4 | * Front-end middleware
5 | */
6 | module.exports = (app, options) => {
7 | const isProd = process.env.NODE_ENV === 'production';
8 |
9 | if (isProd) {
10 | const addProdMiddlewares = require('./addProdMiddlewares');
11 | addProdMiddlewares(app, options);
12 | } else {
13 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel');
14 | const addDevMiddlewares = require('./addDevMiddlewares');
15 | addDevMiddlewares(app, webpackConfig);
16 | }
17 |
18 | return app;
19 | };
20 |
--------------------------------------------------------------------------------
/frontend/server/port.js:
--------------------------------------------------------------------------------
1 | const argv = require('./argv');
2 |
3 | module.exports = parseInt(argv.port || process.env.PORT || '3000', 10);
4 |
--------------------------------------------------------------------------------
/notes.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | Todo:
4 | - DONE persist auth info for user login/logout
5 | - add support for tags
6 | -
7 |
8 |
9 |
10 | Comments:
11 |
--------------------------------------------------------------------------------