├── app ├── translations │ └── en.json ├── containers │ ├── App │ │ ├── constants.js │ │ ├── index.js │ │ ├── tests │ │ │ └── selectors.test.js │ │ ├── selectors.js │ │ └── view.js │ ├── HomePage │ │ ├── constants.js │ │ ├── actions.js │ │ ├── sagas.js │ │ ├── tests │ │ │ ├── reducer.test.js │ │ │ ├── index.test.js │ │ │ ├── selectors.test.js │ │ │ ├── sagas.test.js │ │ │ └── actions.test.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── index.js │ │ ├── selectors.js │ │ └── view.js │ ├── LanguageProvider │ │ ├── constants.js │ │ ├── actions.js │ │ ├── view.js │ │ ├── selectors.js │ │ ├── reducer.js │ │ └── index.js │ └── NotFoundPage │ │ ├── view.js │ │ ├── messages.js │ │ └── index.js ├── favicon.ico ├── components │ ├── H1 │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── A │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Badge │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── Card │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── AppBar │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── Avatar │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── CardText │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── CardHeader │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── CardMedia │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── CardTitle │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── AutoComplete │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── CardActions │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ ├── BottomNavigation │ │ ├── tests │ │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js │ └── BottomNavigationItem │ │ ├── tests │ │ └── index.test.js │ │ ├── view.js │ │ └── index.js ├── global-styles.js ├── tests │ └── store.test.js ├── manifest.json ├── index.html ├── i18n.js ├── reducers.js ├── .nginx.conf ├── .htaccess ├── routes.js ├── store.js ├── utils │ ├── asyncInjectors.js │ └── tests │ │ └── asyncInjectors.test.js └── app.js ├── Procfile ├── internals ├── generators │ ├── language │ │ ├── translations-json.hbs │ │ ├── app-locale.hbs │ │ ├── add-locale-data.hbs │ │ ├── polyfill-intl-locale.hbs │ │ ├── intl-locale-data.hbs │ │ ├── translation-messages.hbs │ │ ├── format-translation-messages.hbs │ │ └── index.js │ ├── container │ │ ├── constants.js.hbs │ │ ├── actions.js.hbs │ │ ├── sagas.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── test.js.hbs │ │ ├── messages.js.hbs │ │ ├── sagas.test.js.hbs │ │ ├── selectors.test.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── reducer.js.hbs │ │ ├── selectors.js.hbs │ │ ├── index.js.hbs │ │ ├── view.js.hbs │ │ └── index.js │ ├── route │ │ ├── route.hbs │ │ ├── routeWithReducer.hbs │ │ └── index.js │ ├── component │ │ ├── es6.js.hbs │ │ ├── es6.pure.js.hbs │ │ ├── test.js.hbs │ │ ├── messages.js.hbs │ │ ├── view.js.hbs │ │ └── index.js │ ├── utils │ │ └── componentExists.js │ └── index.js ├── scripts │ ├── helpers │ │ ├── checkmark.js │ │ └── progress.js │ ├── npmcheckversion.js │ ├── pagespeed.js │ ├── analyze.js │ ├── dependencies.js │ └── extract-intl.js ├── testing │ ├── test-bundler.js │ └── karma.conf.js ├── webpack │ ├── webpack.dll.babel.js │ ├── webpack.prod.babel.js │ ├── webpack.test.babel.js │ ├── webpack.base.babel.js │ └── webpack.dev.babel.js └── config.js ├── .lgtm ├── .editorconfig ├── docs ├── general │ ├── webstorm-debug.png │ ├── webstorm-eslint.png │ ├── server-configs.md │ ├── gotchas.md │ ├── deployment.md │ ├── files.md │ ├── remove.md │ ├── commands.md │ ├── README.md │ └── faq.md ├── css │ ├── remove.md │ ├── README.md │ ├── sanitize.md │ ├── sass.md │ └── styled-componets.md ├── testing │ ├── remote-testing.md │ ├── README.md │ └── component-testing.md ├── js │ ├── README.md │ ├── redux.md │ ├── remove.md │ ├── reselect.md │ ├── redux-saga.md │ ├── i18n.md │ ├── immutablejs.md │ └── routing.md └── README.md ├── .gitignore ├── .travis.yml ├── .babelrc ├── LICENSE ├── server ├── logger.js ├── index.js └── middlewares │ └── frontendMiddleware.js ├── appveyor.yml ├── README.md ├── .gitattributes ├── .eslintrc.json └── package.json /app/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start:prod 2 | -------------------------------------------------------------------------------- /internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 '{{language}}', 2 | -------------------------------------------------------------------------------- /.lgtm: -------------------------------------------------------------------------------- 1 | approvals = 2 2 | pattern = "(?i):shipit:|LGTM" 3 | self_approval_off = true 4 | -------------------------------------------------------------------------------- /internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | */ 4 | 5 | export const DEFAULT_LOCALE = 'en'; 6 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelsonic/react-redux-material-ui-boilerplate/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /docs/general/webstorm-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelsonic/react-redux-material-ui-boilerplate/HEAD/docs/general/webstorm-debug.png -------------------------------------------------------------------------------- /internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /docs/general/webstorm-eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kelsonic/react-redux-material-ui-boilerplate/HEAD/docs/general/webstorm-eslint.png -------------------------------------------------------------------------------- /app/containers/HomePage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/Homepage/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /app/components/H1/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const H1 = styled.h1` 4 | font-size: 2em; 5 | margin-bottom: 0.25em; 6 | `; 7 | 8 | export default H1; 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/css/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing `sanitize.css` 2 | 3 | Delete [lines 31 and 32 in `app.js`](../../app/app.js#L31-L32) and remove it 4 | from the `dependencies` in [`package.json`](../../package.json)! 5 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | export const DEFAULT_LOCALE = 'en'; 9 | -------------------------------------------------------------------------------- /app/containers/HomePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /app/components/A/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A link to a certain page, an anchor tag 3 | */ 4 | 5 | import styled from 'styled-components'; 6 | 7 | const A = styled.a` 8 | color: #41addd; 9 | 10 | &:hover { 11 | color: #6cc0e5; 12 | } 13 | `; 14 | 15 | export default A; 16 | -------------------------------------------------------------------------------- /app/containers/HomePage/sagas.js: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/view.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import messages from './messages'; 4 | 5 | export default function render() { 6 | return ( 7 |

8 | 9 |

10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(location, cb) { 5 | System.import('{{{directory (properCase component)}}}') 6 | .then(loadModule(cb)) 7 | .catch(errorLoading); 8 | }, 9 | },$1 10 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | */ 4 | 5 | import { defineMessages } from 'react-intl'; 6 | 7 | export default defineMessages({ 8 | header: { 9 | id: 'app.components.NotFoundPage.header', 10 | defaultMessage: 'This is NotFoundPage component !', 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import view from './view'; 9 | 10 | class {{ properCase name }} extends Component {} 11 | 12 | {{ properCase name }}.prototype.render = view; 13 | 14 | export default {{ properCase name }}; 15 | -------------------------------------------------------------------------------- /app/components/Badge/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Badge from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/Card/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Card from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import homepageReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('homepageReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(homepageReducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/components/AppBar/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import AppBar from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/Avatar/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import Avatar from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | */ 6 | 7 | import { PureComponent, PropTypes } from 'react'; 8 | import view from './view'; 9 | 10 | export default class App extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | }; 14 | } 15 | 16 | App.prototype.render = view; 17 | -------------------------------------------------------------------------------- /app/components/CardText/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardText from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | */ 6 | 7 | import { PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | export default class NotFound extends PureComponent {} 11 | 12 | NotFound.prototype.render = view; 13 | -------------------------------------------------------------------------------- /internals/generators/component/es6.pure.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import { PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class {{ properCase name }} extends PureComponent {} 11 | 12 | {{ properCase name }}.prototype.render = view; 13 | 14 | export default {{ properCase name }}; 15 | -------------------------------------------------------------------------------- /app/components/CardHeader/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardHeader from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardMedia/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardMedia from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardTitle/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardTitle from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import { Homepage } from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/AutoComplete/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import AutoComplete from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/CardActions/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import CardActions from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/BottomNavigation/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import BottomNavigation from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { {{ properCase name }} } from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/components/BottomNavigationItem/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import BottomNavigationItem from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { selectHomepageDomain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = selectHomepageDomain(); 6 | 7 | describe('selectHomepageDomain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Homepage Messages 3 | * 4 | * This contains all the text for the Homepage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.Homepage.header', 11 | defaultMessage: 'This is Homepage container! (This is coming from Homepage/messages.js)', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/BottomNavigation/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigation view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { BottomNavigation } from 'material-ui/BottomNavigation'; 9 | 10 | export default function render() { 11 | return ( 12 | 15 | {this.props.children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /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 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.{{properCase name }}.header', 11 | defaultMessage: 'This is {{properCase name}} container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/sagas.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 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 | -------------------------------------------------------------------------------- /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 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.{{ properCase name }}.header', 11 | defaultMessage: 'This is the {{ properCase name}} component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/BottomNavigationItem/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigationItem view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { BottomNavigationItem } from 'material-ui/BottomNavigation'; 9 | 10 | export default function render() { 11 | return ( 12 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/view.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | import { IntlProvider } from 'react-intl'; 3 | 4 | export default function render() { 5 | return ( 6 | 11 | {Children.only(this.props.children)} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 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 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { select{{ properCase name }}Domain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = select{{ properCase name}}Domain(); 6 | 7 | describe('select{{ properCase name }}Domain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/containers/HomePage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function homepageReducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default homepageReducer; 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 5 6 | - 4 7 | 8 | script: npm run build 9 | 10 | install: 11 | - npm i -g npm@latest 12 | - npm install 13 | 14 | before_install: 15 | - export CHROME_BIN=chromium-browser 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | 19 | notifications: 20 | email: 21 | on_failure: change 22 | 23 | after_success: 'npm run coveralls' 24 | 25 | cache: 26 | directories: 27 | - node_modules 28 | -------------------------------------------------------------------------------- /app/components/CardText/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardText view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardText } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 18 | {this.props.children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = () => (state) => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | const selectLocale = () => createSelector( 12 | selectLanguage(), 13 | (languageState) => languageState.get('locale') 14 | ); 15 | 16 | export { 17 | selectLanguage, 18 | selectLocale, 19 | }; 20 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('Homepage actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /docs/css/README.md: -------------------------------------------------------------------------------- 1 | # CSS 2 | 3 | This boilerplate uses [`styled-components`](https://github.com/styled-components/styled-components) 4 | allowing you to write your CSS in your JavaScript, 5 | removing the mapping between styles and components. 6 | 7 | `styled-components` let's us embrace component encapsulation while sanitize.css gives us 8 | data-driven cross-browser normalisation. 9 | 10 | Learn more: 11 | 12 | - [`syled-components`](styled-componets.md) 13 | - [sanitize.css](sanitize.md) 14 | - [Using Sass](sass.md) 15 | -------------------------------------------------------------------------------- /app/components/CardActions/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardActions view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardActions } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 18 | {this.props.children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /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 pageComponents = fs.readdirSync('app/components'); 9 | const pageContainers = fs.readdirSync('app/containers'); 10 | const components = pageComponents.concat(pageContainers); 11 | 12 | function componentExists(comp) { 13 | return components.indexOf(comp) >= 0; 14 | } 15 | 16 | module.exports = componentExists; 17 | -------------------------------------------------------------------------------- /app/containers/App/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { selectLocationState } from 'containers/App/selectors'; 5 | 6 | describe('selectLocationState', () => { 7 | it('should select the route as a plain JS object', () => { 8 | const route = fromJS({ 9 | locationBeforeTransitions: null, 10 | }); 11 | const mockedState = fromJS({ 12 | route, 13 | }); 14 | expect(selectLocationState()(mockedState)).toEqual(route.toJS()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/components/Badge/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Badge view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Badge from 'material-ui/Badge'; 9 | 10 | export default function render() { 11 | return ( 12 | 20 | {this.props.children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/components/BottomNavigation/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigation 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class BottomNavigation extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | selectedIndex: PropTypes.number, 14 | }; 15 | 16 | static defaultProps = { 17 | children: undefined, 18 | selectedIndex: undefined, 19 | }; 20 | } 21 | 22 | BottomNavigation.prototype.render = view; 23 | 24 | export default BottomNavigation; 25 | -------------------------------------------------------------------------------- /app/containers/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage 4 | * 5 | */ 6 | 7 | import { PureComponent } from 'react'; 8 | import { connect } from 'react-redux'; 9 | import view from './view'; 10 | import selectHomepage from './selectors'; 11 | 12 | class Homepage extends PureComponent {} 13 | 14 | Homepage.prototype.render = view; 15 | 16 | const mapStateToProps = selectHomepage(); 17 | 18 | function mapDispatchToProps(dispatch) { 19 | return { 20 | dispatch, 21 | }; 22 | } 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(Homepage); 25 | -------------------------------------------------------------------------------- /app/containers/HomePage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the homepage state domain 5 | */ 6 | const selectHomepageDomain = () => (state) => state.get('homepage'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by Homepage 15 | */ 16 | 17 | const selectHomepage = () => createSelector( 18 | selectHomepageDomain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default selectHomepage; 23 | export { 24 | selectHomepageDomain, 25 | }; 26 | -------------------------------------------------------------------------------- /app/components/Avatar/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Avatar view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Avatar from 'material-ui/Avatar'; 9 | 10 | export default function render() { 11 | return ( 12 | 21 | {this.props.children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | // selectLocationState expects a plain JS object for the routing state 2 | const selectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | selectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | // If we need to use Chai, we'll have already chaiEnzyme loaded 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | chai.use(chaiEnzyme()); 9 | 10 | // Include all .js files under `app`, except app.js, reducers.js, and routes.js. 11 | // This is for code coverage 12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes)).)*\.js$/); 13 | context.keys().forEach(context); 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "latest", 5 | { 6 | "es2015": { 7 | "modules": false 8 | } 9 | } 10 | ], 11 | "react", 12 | "stage-0" 13 | ], 14 | "env": { 15 | "production": { 16 | "only": [ 17 | "app" 18 | ], 19 | "plugins": [ 20 | "transform-react-remove-prop-types", 21 | "transform-react-constant-elements", 22 | "transform-react-inline-elements" 23 | ] 24 | }, 25 | "test": { 26 | "plugins": [ 27 | "istanbul" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/BottomNavigationItem/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * BottomNavigationItem 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class BottomNavigationItem extends PureComponent { 11 | static propTypes = { 12 | label: PropTypes.string, 13 | icon: PropTypes.node, 14 | onTouchTap: PropTypes.func, 15 | }; 16 | 17 | static defaultProps = { 18 | label: undefined, 19 | icon: undefined, 20 | onTouchTap: () => {}, 21 | }; 22 | } 23 | 24 | BottomNavigationItem.prototype.render = view; 25 | 26 | export default BottomNavigationItem; 27 | -------------------------------------------------------------------------------- /docs/css/sanitize.md: -------------------------------------------------------------------------------- 1 | # `sanitize.css` 2 | 3 | Sanitize.css makes browsers render elements more in 4 | line with developer expectations (e.g. having the box model set to a cascading 5 | `box-sizing: border-box`) and preferences (its defaults can be individually 6 | overridden). 7 | 8 | It was selected over older projects like `normalize.css` and `reset.css` due 9 | to its greater flexibility and better alignment with CSSNext features like CSS 10 | variables. 11 | 12 | See the [official documentation](https://github.com/10up/sanitize.css) for more 13 | information. 14 | 15 | --- 16 | 17 | _Don't like this feature? [Click here](remove.md)_ 18 | -------------------------------------------------------------------------------- /app/components/Card/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Card view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Card } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 21 | {this.props.children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | CHANGE_LOCALE, 10 | } from './constants'; 11 | import { 12 | DEFAULT_LOCALE, 13 | } from '../App/constants'; 14 | 15 | const initialState = fromJS({ 16 | locale: DEFAULT_LOCALE, 17 | }); 18 | 19 | function languageProviderReducer(state = initialState, action) { 20 | switch (action.type) { 21 | case CHANGE_LOCALE: 22 | return state 23 | .set('locale', action.locale); 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default languageProviderReducer; 30 | -------------------------------------------------------------------------------- /internals/generators/component/view.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | {{#if wantMessages}} 9 | import { FormattedMessage } from 'react-intl'; 10 | import messages from './messages'; 11 | {{/if}} 12 | {{#if wantCSS}} 13 | import styles from './styles.css'; 14 | {{/if}} 15 | 16 | export default function render() { 17 | return ( 18 | {{#if wantCSS}} 19 |
20 | {{else}} 21 |
22 | {{/if}} 23 | {{#if wantMessages}} 24 | 25 | {{/if}} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/components/H1/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import H1 from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('

', () => { 8 | it('should render a prop', () => { 9 | const id = 'testId'; 10 | const renderedComponent = shallow( 11 |

12 | ); 13 | expect(renderedComponent.prop('id')).toEqual(id); 14 | }); 15 | 16 | it('should render its text', () => { 17 | const children = 'Text'; 18 | const renderedComponent = shallow( 19 |

{children}

20 | ); 21 | expect(renderedComponent.contains(children)).toEqual(true); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => (state) => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const select{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default select{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | 3 | /* eslint no-unused-expressions: 0 */ 4 | injectGlobal` 5 | html, 6 | body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 13 | } 14 | 15 | body.fontLoaded { 16 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #app { 20 | background-color: #fafafa; 21 | min-height: 100%; 22 | min-width: 100%; 23 | } 24 | 25 | p, 26 | label { 27 | font-family: Georgia, Times, 'Times New Roman', serif; 28 | line-height: 1.5em; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /app/components/CardText/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardText 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardText extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | color: PropTypes.string, 15 | expandable: PropTypes.bool, 16 | style: PropTypes.object, 17 | }; 18 | 19 | static defaultProps = { 20 | actAsExpander: undefined, 21 | children: undefined, 22 | color: undefined, 23 | expandable: undefined, 24 | style: undefined, 25 | }; 26 | } 27 | 28 | CardText.prototype.render = view; 29 | 30 | export default CardText; 31 | -------------------------------------------------------------------------------- /app/components/CardMedia/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardMedia view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardMedia } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 22 | {this.props.children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/components/CardActions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardActions 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardActions extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | expandable: PropTypes.bool, 15 | showExpandableButton: PropTypes.bool, 16 | style: PropTypes.object, 17 | }; 18 | 19 | static defaultProps = { 20 | actAsExpander: undefined, 21 | children: undefined, 22 | expandable: undefined, 23 | showExpandableButton: undefined, 24 | style: undefined, 25 | }; 26 | } 27 | 28 | CardActions.prototype.render = view; 29 | 30 | export default CardActions; 31 | -------------------------------------------------------------------------------- /docs/general/server-configs.md: -------------------------------------------------------------------------------- 1 | # Server Configurations 2 | 3 | ## Apache 4 | 5 | This boilerplate includes a `.htaccess` file that does two things: 6 | 7 | 1. Redirect all traffic to HTTPS because ServiceWorker only works for encrypted 8 | traffic. 9 | 1. Rewrite all pages (e.g. `yourdomain.com/subpage`) to `yourdomain.com/index.html` 10 | to let `react-router` take care of presenting the correct page. 11 | 12 | > Note: For performance reasons you should probably adapt it to run as a static 13 | `.conf` file (typically under `/etc/apache2/sites-enabled` or similar) so that 14 | your server doesn't have to apply its rules dynamically per request) 15 | 16 | ## Nginx 17 | 18 | Also it includes a `.nginx.conf` file that does the same on Nginx server. 19 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; // eslint-disable-line 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Boilerplate", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } -------------------------------------------------------------------------------- /app/components/CardTitle/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardTitle view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardTitle } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 24 | {this.props.children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/components/Badge/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Badge 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class Badge extends PureComponent { 11 | static propTypes = { 12 | badgeContent: PropTypes.node, 13 | badgeStyle: PropTypes.object, 14 | children: PropTypes.node, 15 | className: PropTypes.string, 16 | primary: PropTypes.bool, 17 | secondary: PropTypes.bool, 18 | style: PropTypes.object, 19 | }; 20 | 21 | static defaultProps = { 22 | badgeContent: undefined, 23 | badgeStyle: undefined, 24 | children: undefined, 25 | className: undefined, 26 | primary: false, 27 | secondary: false, 28 | style: undefined, 29 | }; 30 | } 31 | 32 | Badge.prototype.render = view; 33 | 34 | export default Badge; 35 | -------------------------------------------------------------------------------- /app/containers/App/view.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | // Refer to http://www.material-ui.com/#/get-started/server-rendering for information on material-ui's Mui. 3 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | import { 6 | green100, 7 | green500, 8 | green700, 9 | } from 'material-ui/styles/colors'; 10 | 11 | const muiTheme = getMuiTheme({ 12 | palette: { 13 | primary1Color: green500, 14 | primary2Color: green700, 15 | primary3Color: green100, 16 | }, 17 | }); 18 | 19 | export default function render() { 20 | return ( 21 | 22 |
23 | {Children.toArray(this.props.children)} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.stdin.resume(); 4 | process.stdin.setEncoding('utf8'); 5 | 6 | const ngrok = require('ngrok'); 7 | const psi = require('psi'); 8 | const chalk = require('chalk'); 9 | 10 | log('\nStarting ngrok tunnel'); 11 | 12 | startTunnel(runPsi); 13 | 14 | function runPsi(url) { 15 | log('\nStarting PageSpeed Insights'); 16 | psi.output(url).then(function (err) { 17 | process.exit(0); 18 | }); 19 | } 20 | 21 | function startTunnel(cb) { 22 | ngrok.connect(3000, function (err, url) { 23 | if (err) { 24 | log(chalk.red('\nERROR\n' + err)); 25 | process.exit(0); 26 | } 27 | 28 | log('\nServing tunnel from: ' + chalk.magenta(url)); 29 | cb(url); 30 | }); 31 | } 32 | 33 | function log(string) { 34 | process.stdout.write(string); 35 | } 36 | -------------------------------------------------------------------------------- /app/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Avatar 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class Avatar extends PureComponent { 11 | static propTypes = { 12 | backgroundColor: PropTypes.string, 13 | children: PropTypes.node, 14 | className: PropTypes.string, 15 | color: PropTypes.string, 16 | icon: PropTypes.node, 17 | size: PropTypes.number, 18 | src: PropTypes.string, 19 | style: PropTypes.object, 20 | }; 21 | 22 | static defaultProps = { 23 | backgroundColor: undefined, 24 | children: undefined, 25 | className: undefined, 26 | color: undefined, 27 | icon: undefined, 28 | size: 40, 29 | src: undefined, 30 | style: undefined, 31 | }; 32 | } 33 | 34 | Avatar.prototype.render = view; 35 | 36 | export default Avatar; 37 | -------------------------------------------------------------------------------- /docs/general/gotchas.md: -------------------------------------------------------------------------------- 1 | # Gotchas 2 | 3 | These are some things to be aware of when using this boilerplate. 4 | 5 | ## Special images in HTML files 6 | 7 | If you specify your images in the `.html` files using the `` tag, everything 8 | will work fine. The problem comes up if you try to include images using anything 9 | except that tag, like meta tags: 10 | 11 | ```HTML 12 | 13 | ``` 14 | 15 | The webpack `html-loader` does not recognise this as an image file and will not 16 | transfer the image to the build folder. To get webpack to transfer them, you 17 | have to import them with the file loader in your JavaScript somewhere, e.g.: 18 | 19 | ```JavaScript 20 | import 'file?name=[name].[ext]!../img/yourimg.png'; 21 | ``` 22 | 23 | Then webpack will correctly transfer the image to the build folder. 24 | -------------------------------------------------------------------------------- /internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import { {{{ component }}} } from 'react'; 8 | import { connect } from 'react-redux'; 9 | import view from './view'; 10 | {{#if wantActionsAndReducer}} 11 | import select{{properCase name}} from './selectors'; 12 | {{/if}} 13 | 14 | class {{ properCase name }} extends {{{ component }}} {} 15 | 16 | {{ properCase name }}.prototype.render = view; 17 | 18 | {{#if wantActionsAndReducer}} 19 | const mapStateToProps = select{{properCase name}}(); 20 | {{/if}} 21 | 22 | function mapDispatchToProps(dispatch) { 23 | return { 24 | dispatch, 25 | }; 26 | } 27 | 28 | {{#if wantActionsAndReducer}} 29 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 30 | {{else}} 31 | export default connect(null, mapDispatchToProps)({{ properCase name }}); 32 | {{/if}} 33 | -------------------------------------------------------------------------------- /app/components/Card/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Card 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class Card extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | containerStyle: PropTypes.object, 14 | expandable: PropTypes.bool, 15 | expanded: PropTypes.bool, 16 | initiallyExpanded: PropTypes.bool, 17 | onExpandChange: PropTypes.function, 18 | showExpandableButton: PropTypes.bool, 19 | style: PropTypes.object, 20 | }; 21 | 22 | static defaultProps = { 23 | children: undefined, 24 | containerStyle: undefined, 25 | expandable: false, 26 | expanded: null, 27 | initiallyExpanded: false, 28 | onExpandChange: undefined, 29 | showExpandableButton: undefined, 30 | style: undefined, 31 | }; 32 | } 33 | 34 | Card.prototype.render = view; 35 | 36 | export default Card; 37 | -------------------------------------------------------------------------------- /app/components/CardHeader/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardHeader view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { CardHeader } from 'material-ui/Card'; 9 | 10 | export default function render() { 11 | return ( 12 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /docs/css/sass.md: -------------------------------------------------------------------------------- 1 | # Can I use Sass with this boilerplate? 2 | 3 | Yes, although we advise against it and **do not support this**. We selected 4 | [`styled-components`](https://github.com/styled-components/styled-components) 5 | over Sass because its approach is more powerful: instead of trying to 6 | give a styling language programmatic abilities, it pulls logic and configuration 7 | out into JS where we believe those features belong. 8 | 9 | If you _really_ still want (or need) to use Sass then... 10 | 11 | 1. You will need to add a [sass-loader](https://github.com/jtangelder/sass-loader) 12 | to the loaders section in `internals/webpack/webpack.base.babel.js` so it reads something like 13 | ```javascript 14 | { 15 | test: /\.scss$/, 16 | exclude: /node_modules/, 17 | loaders: ['style', 'css', 'sass'] 18 | } 19 | ``` 20 | 21 | Then run `npm i -D sass-loader node-sass` 22 | 23 | ...and you should be good to go! 24 | -------------------------------------------------------------------------------- /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 ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | System.import('containers/{{ properCase component }}/reducer'), 7 | {{#if useSagas}} 8 | System.import('containers/{{ properCase component }}/sagas'), 9 | {{/if}} 10 | System.import('containers/{{ properCase component }}'), 11 | ]); 12 | 13 | const renderRoute = loadModule(cb); 14 | 15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => { 16 | injectReducer('{{ camelCase component }}', reducer.default); 17 | {{#if useSagas}} 18 | injectSagas(sagas.default); 19 | {{/if}} 20 | renderRoute(component); 21 | }); 22 | 23 | importModules.catch(errorLoading); 24 | }, 25 | },$1 26 | -------------------------------------------------------------------------------- /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 componentGenerator = require('./component/index.js'); 9 | const containerGenerator = require('./container/index.js'); 10 | const routeGenerator = require('./route/index.js'); 11 | const languageGenerator = require('./language/index.js'); 12 | 13 | module.exports = (plop) => { 14 | plop.setGenerator('component', componentGenerator); 15 | plop.setGenerator('container', containerGenerator); 16 | plop.setGenerator('route', routeGenerator); 17 | plop.setGenerator('language', languageGenerator); 18 | plop.addHelper('directory', (comp) => { 19 | try { 20 | fs.accessSync(`app/containers/${comp}`, fs.F_OK); 21 | return `containers/${comp}`; 22 | } catch (e) { 23 | return `components/${comp}`; 24 | } 25 | }); 26 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 27 | }; 28 | -------------------------------------------------------------------------------- /app/components/CardMedia/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardMedia 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardMedia extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | expandable: PropTypes.bool, 15 | mediaStyle: PropTypes.object, 16 | overlay: PropTypes.node, 17 | overlayContainerStyle: PropTypes.object, 18 | overlayContentStyle: PropTypes.object, 19 | overlayStyle: PropTypes.object, 20 | style: PropTypes.object, 21 | }; 22 | 23 | static defaultProps = { 24 | actAsExpander: undefined, 25 | children: undefined, 26 | expandable: undefined, 27 | mediaStyle: undefined, 28 | overlay: undefined, 29 | overlayContainerStyle: undefined, 30 | overlayContentStyle: undefined, 31 | overlayStyle: undefined, 32 | style: undefined, 33 | }; 34 | } 35 | 36 | CardMedia.prototype.render = view; 37 | 38 | export default CardMedia; 39 | -------------------------------------------------------------------------------- /internals/generators/container/view.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | {{#if wantHeaders}} 9 | import Helmet from 'react-helmet'; 10 | {{/if}} 11 | {{#if wantMessages}} 12 | import { FormattedMessage } from 'react-intl'; 13 | import messages from './messages'; 14 | {{/if}} 15 | {{#if wantCSS}} 16 | import styles from './styles.css'; 17 | {{/if}} 18 | 19 | export default function render() { 20 | return ( 21 | {{#if wantCSS}} 22 |
23 | {{else}} 24 |
25 | {{/if}} 26 | {{#if wantHeaders}} 27 | 33 | {{/if}} 34 | {{#if wantMessages}} 35 | 36 | {{/if}} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import { PureComponent, PropTypes } from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { selectLocale } from './selectors'; 13 | import view from './view'; 14 | 15 | class LanguageProvider extends PureComponent { 16 | static propTypes = { 17 | locale: PropTypes.string, 18 | messages: PropTypes.object, 19 | children: PropTypes.element.isRequired, 20 | }; 21 | } 22 | 23 | LanguageProvider.prototype.render = view; 24 | 25 | const mapStateToProps = createSelector( 26 | selectLocale(), 27 | (locale) => ({ locale }) 28 | ); 29 | 30 | function mapDispatchToProps(dispatch) { 31 | return { 32 | dispatch, 33 | }; 34 | } 35 | 36 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 37 | -------------------------------------------------------------------------------- /app/components/AppBar/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AppBar view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import AppBar from 'material-ui/AppBar'; 9 | 10 | export default function render() { 11 | return ( 12 | 29 | {this.props.children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/components/CardTitle/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardTitle 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardTitle extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | children: PropTypes.node, 14 | expandable: PropTypes.bool, 15 | showExpandableButton: PropTypes.bool, 16 | style: PropTypes.object, 17 | subtitle: PropTypes.node, 18 | subtitleColor: PropTypes.string, 19 | subtitleStyle: PropTypes.object, 20 | title: PropTypes.node, 21 | titleColor: PropTypes.string, 22 | titleStyle: PropTypes.object, 23 | }; 24 | 25 | static defaultProps = { 26 | actAsExpander: undefined, 27 | children: undefined, 28 | expandable: undefined, 29 | showExpandableButton: undefined, 30 | style: undefined, 31 | subtitle: undefined, 32 | subtitleColor: undefined, 33 | subtitleStyle: undefined, 34 | title: undefined, 35 | titleColor: undefined, 36 | titleStyle: undefined, 37 | }; 38 | } 39 | 40 | CardTitle.prototype.render = view; 41 | 42 | export default CardTitle; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Kelson Adams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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').dllPlugin; 16 | 17 | if (!pkg.dllPlugin) { process.exit(0); } 18 | 19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 20 | const outputPath = join(process.cwd(), dllConfig.path); 21 | 22 | module.exports = require('./webpack.base.babel')({ 23 | context: process.cwd(), 24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 25 | devtool: 'eval', 26 | output: { 27 | filename: '[name].dll.js', 28 | path: outputPath, 29 | library: '[name]', 30 | }, 31 | plugins: [ 32 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new 33 | ], 34 | }); 35 | -------------------------------------------------------------------------------- /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 | 13 | // Called whenever there's an error on the server we want to print 14 | error: (err) => { 15 | console.error(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, tunnelStarted) => { 20 | console.log(`Server started ${chalk.green('✓')}`); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 25 | } 26 | 27 | console.log(` 28 | ${chalk.bold('Access URLs:')}${divider} 29 | Localhost: ${chalk.magenta(`http://localhost:${port}`)} 30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider} 32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 33 | `); 34 | }, 35 | }; 36 | 37 | module.exports = logger; 38 | -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | import { DEFAULT_LOCALE } from './containers/App/constants'; // eslint-disable-line 9 | 10 | import enLocaleData from 'react-intl/locale-data/en'; 11 | 12 | export const appLocales = [ 13 | 'en', 14 | ]; 15 | 16 | import enTranslationMessages from './translations/en.json'; 17 | 18 | addLocaleData(enLocaleData); 19 | 20 | export const formatTranslationMessages = (locale, messages) => { 21 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {}; 22 | const formattedMessages = {}; 23 | const messageKeys = Object.keys(messages); 24 | for (const messageKey of messageKeys) { 25 | if (locale === DEFAULT_LOCALE) { 26 | formattedMessages[messageKey] = messages[messageKey]; 27 | } else { 28 | formattedMessages[messageKey] = messages[messageKey] || defaultFormattedMessages[messageKey]; 29 | } 30 | } 31 | 32 | return formattedMessages; 33 | }; 34 | 35 | export const translationMessages = { 36 | en: formatTranslationMessages('en', enTranslationMessages), 37 | }; 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | const setup = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false; 10 | const resolve = require('path').resolve; 11 | const app = express(); 12 | 13 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 14 | // app.use('/api', myApi); 15 | 16 | // In production we need to pass these values in instead of relying on webpack 17 | setup(app, { 18 | outputPath: resolve(process.cwd(), 'build'), 19 | publicPath: '/', 20 | }); 21 | 22 | // get the intended port number, use port 3000 if not provided 23 | const port = argv.port || process.env.PORT || 3000; 24 | 25 | // Start your app. 26 | app.listen(port, (err) => { 27 | if (err) { 28 | return logger.error(err.message); 29 | } 30 | 31 | // Connect to ngrok in dev mode 32 | if (ngrok) { 33 | ngrok.connect(port, (innerErr, url) => { 34 | if (innerErr) { 35 | return logger.error(innerErr); 36 | } 37 | 38 | logger.appStarted(port, url); 39 | }); 40 | } else { 41 | logger.appStarted(port); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: 6 15 | - nodejs_version: 5 16 | - nodejs_version: 4 17 | 18 | # Fix line endings in Windows. (runs before repo cloning) 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | # Install scripts--runs after repo cloning 23 | install: 24 | # Install chrome 25 | - choco install -y googlechrome 26 | # Install the latest stable version of Node 27 | - ps: Install-Product node $env:nodejs_version 28 | - npm -g install npm 29 | - set PATH=%APPDATA%\npm;%PATH% 30 | - npm install 31 | 32 | # Disable automatic builds 33 | build: off 34 | 35 | # Post-install test scripts 36 | test_script: 37 | # Output debugging info 38 | - node --version 39 | - npm --version 40 | # run build and run tests 41 | - npm run build 42 | 43 | # Cache node_modules for faster builds 44 | cache: 45 | - node_modules -> package.json 46 | 47 | # remove, as appveyor doesn't support secure variables on pr builds 48 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 49 | #on_success: 50 | #- npm run coveralls 51 | -------------------------------------------------------------------------------- /app/components/CardHeader/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CardHeader 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class CardHeader extends PureComponent { 11 | static propTypes = { 12 | actAsExpander: PropTypes.bool, 13 | avatar: PropTypes.node, 14 | children: PropTypes.node, 15 | closeIcon: PropTypes.node, 16 | expandable: PropTypes.bool, 17 | openIcon: PropTypes.node, 18 | showExpandableButton: PropTypes.bool, 19 | style: PropTypes.object, 20 | subtitle: PropTypes.node, 21 | subtitleColor: PropTypes.string, 22 | subtitleStyle: PropTypes.object, 23 | textStyle: PropTypes.object, 24 | title: PropTypes.node, 25 | titleColor: PropTypes.string, 26 | titleStyle: PropTypes.object, 27 | }; 28 | 29 | static defaultProps = { 30 | actAsExpander: undefined, 31 | avatar: null, 32 | children: undefined, 33 | closeIcon: undefined, 34 | expandable: undefined, 35 | openIcon: undefined, 36 | showExpandableButton: undefined, 37 | style: undefined, 38 | subtitle: undefined, 39 | subtitleColor: undefined, 40 | subtitleStyle: undefined, 41 | textStyle: undefined, 42 | title: undefined, 43 | titleColor: undefined, 44 | titleStyle: undefined, 45 | }; 46 | } 47 | 48 | CardHeader.prototype.render = view; 49 | 50 | export default CardHeader; 51 | -------------------------------------------------------------------------------- /app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 10 | 11 | /* 12 | * routeReducer 13 | * 14 | * The reducer merges route location changes into our immutable state. 15 | * The change is necessitated by moving to react-router-redux@4 16 | * 17 | */ 18 | 19 | // Initial routing state 20 | const routeInitialState = fromJS({ 21 | locationBeforeTransitions: null, 22 | }); 23 | 24 | /** 25 | * Merge route into the global application state 26 | */ 27 | function routeReducer(state = routeInitialState, action) { 28 | switch (action.type) { 29 | /* istanbul ignore next */ 30 | case LOCATION_CHANGE: 31 | return state.merge({ 32 | locationBeforeTransitions: action.payload, 33 | }); 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /** 40 | * Creates the main reducer with the asynchronously loaded ones 41 | */ 42 | export default function createReducer(asyncReducers) { 43 | return combineReducers({ 44 | route: routeReducer, 45 | language: languageProviderReducer, 46 | ...asyncReducers, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /docs/js/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 2 | 3 | ## State management 4 | 5 | This boilerplate manages application state using [Redux](redux.md), makes it 6 | immutable with [`ImmutableJS`](immutablejs.md) and keeps access performant 7 | via [`reselect`](reselect.md). 8 | 9 | For managing asynchronous flows (e.g. logging in) we use [`redux-saga`](redux-saga.md). 10 | 11 | For routing, we use [`react-router` in combination with `react-router-redux`](routing.md). 12 | 13 | We include a generator for components, containers, sagas, routes and selectors. 14 | Run `npm run generate` to choose from the available generators, and automatically 15 | add new parts of your application! 16 | 17 | > Note: If you want to skip the generator selection process, 18 | `npm run generate ` also works. (e.g. `npm run generate route`) 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 | 29 | ## Architecture: `components` and `containers` 30 | 31 | We adopted a split between stateless, reusable components called (wait for it...) 32 | `components` and stateful parent components called `containers`. 33 | 34 | ### Learn more 35 | 36 | See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 37 | by Dan Abramov for a great introduction to this approach. 38 | -------------------------------------------------------------------------------- /docs/css/styled-componets.md: -------------------------------------------------------------------------------- 1 | # `styled-components` 2 | 3 | `styled-components` allow you to write actual CSS code in your JavaScript to style your components, 4 | removing the mapping between components and styles. 5 | 6 | See the 7 | [official documentation](https://github.com/styled-components/styled-components) 8 | for more information! 9 | 10 | ## Usage 11 | 12 | This creates two react components, `` and `<Wrapper>`: 13 | 14 | ```JSX 15 | import React from 'react'; 16 | 17 | import styled from 'styled-components'; 18 | 19 | // Create a <Title> react component that renders an <h1> which is 20 | // centered, palevioletred and sized at 1.5em 21 | const Title = styled.h1` 22 | font-size: 1.5em; 23 | text-align: center; 24 | color: palevioletred; 25 | `; 26 | 27 | // Create a <Wrapper> react component that renders a <section> with 28 | // some padding and a papayawhip background 29 | const Wrapper = styled.section` 30 | padding: 4em; 31 | background: papayawhip; 32 | `; 33 | ``` 34 | 35 | *(The CSS rules are automatically vendor prefixed, so you don't have to think about it!)* 36 | 37 | You render them like so: 38 | 39 | ```JSX 40 | // Use them like any other React component – except they're styled! 41 | <Wrapper> 42 | <Title>Hello World, this is my first styled component! 43 | 44 | ``` 45 | 46 | For further examples see the 47 | [official documentation](https://github.com/styled-components/styled-components). 48 | -------------------------------------------------------------------------------- /app/components/AutoComplete/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AutoComplete view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import AutoComplete from 'material-ui/AutoComplete'; 9 | 10 | export default function render() { 11 | return ( 12 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/AppBar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AppBar 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class AppBar extends PureComponent { 11 | static propTypes = { 12 | children: PropTypes.node, 13 | className: PropTypes.string, 14 | iconClassNameLeft: PropTypes.string, 15 | iconClassNameRight: PropTypes.string, 16 | iconElementLeft: PropTypes.node, 17 | iconElementRight: PropTypes.node, 18 | iconStyleLeft: PropTypes.object, 19 | iconStyleRight: PropTypes.object, 20 | onLeftIconButtonTouchTap: PropTypes.func, 21 | onRightIconButtonTouchTap: PropTypes.func, 22 | onTitleTouchTap: PropTypes.func, 23 | showMenuIconButton: PropTypes.bool, 24 | style: PropTypes.object, 25 | title: PropTypes.node, 26 | titleStyle: PropTypes.object, 27 | zDepth: PropTypes.number, 28 | }; 29 | 30 | static defaultProps = { 31 | children: undefined, 32 | className: undefined, 33 | iconClassNameLeft: undefined, 34 | iconClassNameRight: undefined, 35 | iconElementLeft: undefined, 36 | iconElementRight: undefined, 37 | iconStyleLeft: undefined, 38 | iconStyleRight: undefined, 39 | onLeftIconButtonTouchTap: () => {}, 40 | onRightIconButtonTouchTap: () => {}, 41 | onTitleTouchTap: () => {}, 42 | showMenuIconButton: true, 43 | style: undefined, 44 | title: '', 45 | titleStyle: undefined, 46 | zDepth: 1, 47 | }; 48 | } 49 | 50 | AppBar.prototype.render = view; 51 | 52 | export default AppBar; 53 | -------------------------------------------------------------------------------- /app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # Redirect to the same URL with https:// 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name example.com; 17 | 18 | return 301 https://$server_name$request_uri; 19 | 20 | } 21 | 22 | ## 23 | # HTTPS configurations 24 | ## 25 | 26 | server { 27 | 28 | listen 443; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | ssl on; 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | # Use only TSL protocols for more secure 38 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 39 | 40 | # Always serve index.html for any request 41 | location / { 42 | # Set path 43 | root /var/www/; 44 | try_files $uri /index.html; 45 | } 46 | 47 | ## 48 | # If you want to use Node/Rails/etc. API server 49 | # on the same port (443) config Nginx as a reverse proxy. 50 | # For security reasons use a firewall like ufw in Ubuntu 51 | # and deny port 3000/tcp. 52 | ## 53 | 54 | # location /api/ { 55 | # 56 | # proxy_pass http://localhost:3000; 57 | # proxy_http_version 1.1; 58 | # proxy_set_header X-Forwarded-Proto https; 59 | # proxy_set_header Upgrade $http_upgrade; 60 | # proxy_set_header Connection 'upgrade'; 61 | # proxy_set_header Host $host; 62 | # proxy_cache_bypass $http_upgrade; 63 | # 64 | # } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /docs/general/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Heroku 4 | 5 | ### Easy 5-Step Deployment Process 6 | 7 | *Step 1:* Create a _Procfile_ with the following line: `web: npm run start:prod`. We do this because Heroku runs `npm run start` by default, so we need this setting to override the default run command. 8 | 9 | *Step 2:* Install the Node.js buildpack for your Heroku app by running the following command: `heroku buildpacks:set https://github.com/heroku/heroku-buildpack-nodejs#v91 -a [your app name]`. Make sure to replace `#v91` with whatever the latest buildpack is, which you can [find here](https://github.com/heroku/heroku-buildpack-nodejs/releases). 10 | 11 | *Step 3:* Add this line to your `package.json` file in the scripts area: `"heroku-postbuild": "npm run build",`. This is so Heroku can build your production assets when deploying (more of which you can [read about here](https://devcenter.heroku.com/articles/nodejs-support#heroku-specific-build-steps)). Then, adjust the _prebuild_ script in your `package.json` file so it looks like this: `"prebuild": "npm run build:clean",` to avoid having Heroku attempt to run Karma tests (which are unsupported with this buildpack). 12 | 13 | *Step 4:* Run `heroku config:set NPM_CONFIG_PRODUCTION=false` so that Heroku can compile the NPM modules included in your _devDependencies_ (since many of these packages are required for the build process). 14 | 15 | *Step 5:* Follow the standard Heroku deploy process: 16 | 17 | 1. `git add .` 18 | 2. `git commit -m 'Made some epic changes as per usual'` 19 | 3. `git push heroku master` 20 | -------------------------------------------------------------------------------- /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(defaults({ 35 | name: 'react-boilerplate-dlls', 36 | private: true, 37 | author: pkg.author, 38 | repository: pkg.repository, 39 | version: pkg.version, 40 | }), null, 2), 41 | 'utf8' 42 | ); 43 | } 44 | 45 | // the BUILDING_DLL env var is set to avoid confusing the development environment 46 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js'); 47 | -------------------------------------------------------------------------------- /internals/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const path = require('path'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | frameworks: ['mocha'], 8 | reporters: ['coverage', 'mocha'], 9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | 14 | autoWatch: false, 15 | singleRun: true, 16 | 17 | client: { 18 | mocha: { 19 | grep: argv.grep, 20 | }, 21 | }, 22 | 23 | files: [ 24 | { 25 | pattern: './test-bundler.js', 26 | watched: false, 27 | served: true, 28 | included: true, 29 | }, 30 | ], 31 | 32 | preprocessors: { 33 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key 34 | }, 35 | 36 | webpack: webpackConfig, 37 | 38 | // make Webpack bundle generation quiet 39 | webpackMiddleware: { 40 | noInfo: true, 41 | stats: 'errors-only', 42 | }, 43 | 44 | customLaunchers: { 45 | ChromeTravis: { 46 | base: 'Chrome', 47 | flags: ['--no-sandbox'], 48 | }, 49 | }, 50 | 51 | coverageReporter: { 52 | dir: path.join(process.cwd(), 'coverage'), 53 | reporters: [ 54 | { type: 'lcov', subdir: 'lcov' }, 55 | { type: 'html', subdir: 'html' }, 56 | { type: 'text-summary' }, 57 | ], 58 | }, 59 | 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /docs/general/files.md: -------------------------------------------------------------------------------- 1 | # Configuration: A Glossary 2 | 3 | A guide to the configuration files for this project: where they live and what 4 | they do. 5 | 6 | ## The root folder 7 | 8 | * `.editorconfig`: Sets the default configuration for certain files across editors. (e.g. indentation) 9 | 10 | * `.gitattributes`: Normalizes how `git`, the version control system this boilerplate uses, handles certain files. 11 | 12 | * `.gitignore`: Tells `git` to ignore certain files and folders which don't need to be version controlled, like the build folder. 13 | 14 | * `.travis.yml` and `appveyor.yml`: Continuous Integration configuration
15 | This boilerplate uses [Travis CI](https://travis-ci.com) for Linux environments 16 | and [AppVeyor](https://www.appveyor.com/) for Windows platforms, but feel free 17 | to swap either out for your own choice of CI. 18 | 19 | * `package.json`: Our `npm` configuration file has three functions: 20 | 21 | 1. It's where Babel and ESLint are configured 22 | 1. It's the API for the project: a consistent interface for all its controls 23 | 1. It lists the project's package dependencies 24 | 25 | Baking the config in is a slightly unusual set-up, but it allows us to keep 26 | the project root as uncluttered and grokkable-at-a-glance as possible. 27 | 28 | ## The `./internals` folder 29 | 30 | This is where the bulk of the tooling configuration lives, broken out into 31 | recognisable units of work. 32 | 33 | Feel free to change anything you like but don't be afraid to [ask upfront](https://gitter.im/mxstbr/react-boilerplate) 34 | whether you should: build systems are easy to break! 35 | -------------------------------------------------------------------------------- /app/components/A/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing our link component 3 | */ 4 | 5 | import A from '../index'; 6 | 7 | import expect from 'expect'; 8 | import { shallow } from 'enzyme'; 9 | import React from 'react'; 10 | 11 | const href = 'http://mxstbr.com/'; 12 | const children = (

Test

); 13 | const renderComponent = (props = {}) => shallow( 14 | 15 | {children} 16 | 17 | ); 18 | 19 | describe('', () => { 20 | it('should render an tag', () => { 21 | const renderedComponent = renderComponent(); 22 | expect(renderedComponent.type()).toEqual('a'); 23 | }); 24 | 25 | it('should have an href attribute', () => { 26 | const renderedComponent = renderComponent(); 27 | expect(renderedComponent.prop('href')).toEqual(href); 28 | }); 29 | 30 | it('should have children', () => { 31 | const renderedComponent = renderComponent(); 32 | expect(renderedComponent.contains(children)).toEqual(true); 33 | }); 34 | 35 | it('should have a className attribute', () => { 36 | const className = 'test'; 37 | const renderedComponent = renderComponent({ className }); 38 | expect(renderedComponent.find('a').hasClass(className)).toEqual(true); 39 | }); 40 | 41 | it('should adopt a target attribute', () => { 42 | const target = '_blank'; 43 | const renderedComponent = renderComponent({ target }); 44 | expect(renderedComponent.prop('target')).toEqual(target); 45 | }); 46 | 47 | it('should adopt a type attribute', () => { 48 | const type = 'text/html'; 49 | const renderedComponent = renderComponent({ type }); 50 | expect(renderedComponent.prop('type')).toEqual(type); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/general/remove.md: -------------------------------------------------------------------------------- 1 | ### Removing offline access 2 | 3 | **Careful** about removing this, as there is no real downside to having your 4 | application available when the users network connection isn't perfect. 5 | 6 | To remove offline capability, delete the `offline-plugin` from the 7 | [`package.json`](../../package.json), remove the import of the plugin in 8 | [`app.js`](../../app/app.js) and remove the plugin from the 9 | [`webpack.prod.babel.js`](../../internals/webpack/webpack.prod.babel.js). 10 | 11 | ### Removing add to homescreen functionality 12 | 13 | Delete [`manifest.json`](../../app/manifest.json) and remove the 14 | `` tag from the 15 | [`index.html`](../../app/index.html). 16 | 17 | ### Removing performant web font loading 18 | 19 | **Careful** about removing this, as perceived performance might be highly impacted. 20 | 21 | To remove `FontFaceObserver`, don't import it in [`app.js`](../../app/app.js) and 22 | remove it from the [`package.json`](../../package.json). 23 | 24 | ### Removing image optimization 25 | 26 | To remove image optimization, delete the `image-webpack-loader` from the 27 | [`package.json`](../../package.json), and remove the `image-loader` from [`webpack.base.babel.js`](../../internals/webpack/webpack.base.babel.js): 28 | ``` 29 | … 30 | { 31 | test: /\.(jpg|png|gif)$/, 32 | loaders: [ 33 | 'file-loader', 34 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 35 | ], 36 | } 37 | … 38 | ``` 39 | 40 | Then replace it with classic `file-loader`: 41 | 42 | ``` 43 | … 44 | { 45 | test: /\.(jpg|png|gif)$/, 46 | loader: 'file-loader', 47 | } 48 | … 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/js/redux.md: -------------------------------------------------------------------------------- 1 | # Redux 2 | 3 | If you haven't worked with Redux, it's highly recommended (possibly indispensable!) 4 | to read through the (amazing) [official documentation](http://redux.js.org) 5 | and/or watch this [free video tutorial series](https://egghead.io/series/getting-started-with-redux). 6 | 7 | ## Usage 8 | 9 | See above! As minimal as Redux is, the challenge it addresses - app state 10 | management - is a complex topic that is too involved to properly discuss here. 11 | 12 | ## Removing redux 13 | 14 | There are a few reasons why we chose to bundle redux with React Boilerplate, the 15 | biggest being that it is widely regarded as the current best Flux implementation 16 | in terms of architecture, support and documentation. 17 | 18 | You may feel differently! This is completely OK :) 19 | 20 | Below are a few reasons you might want to remove it: 21 | 22 | ### I'm just getting started and Flux is hard 23 | 24 | You're under no obligation to use Redux or any other Flux library! The complexity 25 | of your application will determine the point at which you need to introduce it. 26 | 27 | Here are a couple of great resources for taking a minimal approach: 28 | 29 | - [Misconceptions of Tooling in JavaScript](http://javascriptplayground.com/blog/2016/02/the-react-webpack-tooling-problem) 30 | - [Learn Raw React — no JSX, no Flux, no ES6, no Webpack…](http://jamesknelson.com/learn-raw-react-no-jsx-flux-es6-webpack/) 31 | 32 | ### It's overkill for my project! 33 | 34 | See above. 35 | 36 | ### I prefer `(Alt|MobX|SomethingElse)`! 37 | 38 | React Boilerplate is a baseline for _your_ app: go for it! 39 | 40 | If you feel that we should take a closer look at supporting your preference 41 | out of the box, please let us know. 42 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 6 | 7 | const errorLoading = (err) => { 8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 9 | }; 10 | 11 | const loadModule = (cb) => (componentModule) => { 12 | cb(null, componentModule.default); 13 | }; 14 | 15 | export default function createRoutes(store) { 16 | // Create reusable async injectors using getAsyncInjectors factory 17 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 18 | 19 | return [ 20 | { 21 | path: '/', 22 | name: 'homepage', 23 | getComponent(nextState, cb) { 24 | const importModules = Promise.all([ 25 | System.import('containers/Homepage/reducer'), 26 | System.import('containers/Homepage/sagas'), 27 | System.import('containers/Homepage'), 28 | ]); 29 | 30 | const renderRoute = loadModule(cb); 31 | 32 | importModules.then(([reducer, sagas, component]) => { 33 | injectReducer('homepage', reducer.default); 34 | injectSagas(sagas.default); 35 | renderRoute(component); 36 | }); 37 | 38 | importModules.catch(errorLoading); 39 | }, 40 | }, { 41 | path: '*', 42 | name: 'notfound', 43 | getComponent(nextState, cb) { 44 | System.import('containers/NotFoundPage') 45 | .then(loadModule(cb)) 46 | .catch(errorLoading); 47 | }, 48 | }, 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React/Redux Boilerplate with Material-UI wrapper components 2 | 3 | This is my main react redux boilerplate with pre-built [material-ui](http://material-ui.com) wrapper components. 4 | 5 | ## Quick start 6 | 7 | 1. Clone this repo using `git clone https://github.com/kelsonic/react-redux-material-ui-boilerplate.git` 8 | 2. Run either `npm install` or `yarn` (**We auto-detect `yarn` on your machine). 9 | 3. Start your development server by running `npm start`. Your server should be running on `localhost: 3000`. 10 | 11 | ## Documentation 12 | 13 | - [Intro](docs/general): What's included and why 14 | - [**Commands**](docs/general/commands.md): Getting the most out of this boilerplate 15 | - [Testing](docs/testing): How to work with the built-in test harness 16 | - [Styling](docs/css): How to work with the CSS tooling 17 | - [Your app](docs/js): Supercharging your app with Routing, Redux, simple 18 | asynchronicity helpers, etc. 19 | 20 | ## Another React-Redux boilerplate? 21 | 22 | A large inspiration came from Max's [React-Redux boilerplate](https://github.com/mxstbr/react-boilerplate), which has been battle-tested and is production-ready. 23 | 24 | *So why create another React-Redux boilerplate?* A few reasons: 25 | 26 | - I needed a separate `view.js` for each component/container. 27 | - [PureComponents are preferred (ie. optimized) over stateless components](https://medium.com/modus-create-front-end-development/component-rendering-performance-in-react-df859b474adc#.8sz7sopg0). 28 | - I needed material-ui wrapper components that could be easily used in containers. 29 | 30 | There are more reasons, though I figured introducing pre-built material-ui wrapper components was reason enough to open-source this boilerplate. 31 | 32 | ## License 33 | 34 | [MIT License](LICENSE). 35 | -------------------------------------------------------------------------------- /app/containers/HomePage/view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Homepage view 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import Helmet from 'react-helmet'; 9 | import { FormattedMessage } from 'react-intl'; 10 | import messages from './messages'; 11 | // Extra material-ui components (don't necessitate wrappers) 12 | import FileFolder from 'material-ui/svg-icons/file/folder'; 13 | // Material-UI wrapper components 14 | import AppBar from 'components/AppBar'; 15 | import AutoComplete from 'components/AutoComplete'; 16 | import Avatar from 'components/Avatar'; 17 | import Badge from 'components/Badge'; 18 | // Other Components 19 | import A from 'components/A'; 20 | import H1 from 'components/H1'; 21 | 22 | export default function render() { 23 | return ( 24 |
25 | 31 | 32 | 33 |

AppBar

34 | 38 | 39 |

AutoComplete

40 | 44 | 45 |

Avatar

46 | } 48 | color={'#999'} 49 | backgroundColor={'#e9e9e9'} 50 | size={30} 51 | /> 52 | 53 |

Badge

54 | 58 | Company Name 59 | 60 | 61 |

BottomNavigation (TODO: example)

62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | 13 | export default function configureStore(initialState = {}, history) { 14 | // Create the store with two middlewares 15 | // 1. sagaMiddleware: Makes redux-sagas work 16 | // 2. routerMiddleware: Syncs the location/URL path to the state 17 | const middlewares = [ 18 | sagaMiddleware, 19 | routerMiddleware(history), 20 | ]; 21 | 22 | const enhancers = [ 23 | applyMiddleware(...middlewares), 24 | ]; 25 | 26 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 27 | /* eslint-disable no-underscore-dangle */ 28 | const composeEnhancers = 29 | process.env.NODE_ENV !== 'production' && 30 | typeof window === 'object' && 31 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 32 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; 33 | /* eslint-enable */ 34 | 35 | const store = createStore( 36 | createReducer(), 37 | fromJS(initialState), 38 | composeEnhancers(...enhancers) 39 | ); 40 | 41 | // Extensions 42 | store.runSaga = sagaMiddleware.run; 43 | store.asyncReducers = {}; // Async reducer registry 44 | 45 | // Make reducers hot reloadable, see http://mxs.is/googmo 46 | /* istanbul ignore next */ 47 | if (module.hot) { 48 | module.hot.accept('./reducers', () => { 49 | System.import('./reducers').then((reducerModule) => { 50 | const createReducers = reducerModule.default; 51 | const nextReducers = createReducers(store.asyncReducers); 52 | 53 | store.replaceReducer(nextReducers); 54 | }); 55 | }); 56 | } 57 | 58 | return store; 59 | } 60 | -------------------------------------------------------------------------------- /internals/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 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.3.3', 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 = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 46 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 47 | const includeDependencies = uniq(dependencyNames.concat(include)); 48 | 49 | return { 50 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 51 | }; 52 | }, 53 | }, 54 | }; 55 | 56 | module.exports = ReactBoilerplate; 57 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /docs/js/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing `redux-saga` 2 | 3 | **We don't recommend removing `redux-saga`**, as we strongly feel that it's the 4 | way to go for most redux based applications. 5 | 6 | If you really want to get rid of it, you will have to delete its traces from several places. 7 | 8 | **app/store.js** 9 | 10 | 1. Remove statement `import createSagaMiddleware from 'redux-saga'`. 11 | 2. Remove statement `const sagaMiddleware = createSagaMiddleware()`. 12 | 3. Remove `sagaMiddleware` from `middlewares` array. 13 | 4. Remove statement `store.runSaga = sagaMiddleware.run` 14 | 15 | **app/utils/asyncInjectors.js** 16 | 17 | 1. Remove `runSaga: isFunction` from `shape`. 18 | 2. Remove function `injectAsyncSagas`. 19 | 3. Do not export `injectSagas: injectAsyncSagas(store, true)`. 20 | 21 | **app/routes.js** 22 | 23 | 1. Do not pull out `injectSagas` from `getAsyncInjectors()`. 24 | 2. Remove `sagas` from `importModules.then()`. 25 | 3. Remove `injectSagas(sagas.default)` from every route that uses Saga. 26 | 27 | **Finally, remove it from the `package.json`. Then you should be good to go with whatever 28 | side-effect management library you want to use!** 29 | 30 | ## Removing `reselect` 31 | 32 | To remove `reselect`, remove it from your dependencies in `package.json` and then write 33 | your `mapStateToProps` functions like you normally would! 34 | 35 | You'll also need to hook up the history directly to the store. Make changes to `app/app.js`. 36 | 37 | 1. Remove statement `import { selectLocationState } from 'containers/App/selectors'` 38 | 2. Make necessary changes to `history` as follows: 39 | 40 | ```js 41 | 42 | const selectLocationState = () => { 43 | let prevRoutingState; 44 | let prevRoutingStateJS; 45 | 46 | return (state) => { 47 | const routingState = state.get('route'); // or state.route 48 | 49 | if (!routingState.equals(prevRoutingState)) { 50 | prevRoutingState = routingState; 51 | prevRoutingStateJS = routingState.toJS(); 52 | } 53 | 54 | return prevRoutingStateJS; 55 | }; 56 | }; 57 | 58 | const history = syncHistoryWithStore(browserHistory, store, { 59 | selectLocationState: selectLocationState(), 60 | }); 61 | ``` 62 | -------------------------------------------------------------------------------- /app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from 'reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | if (Reflect.has(store.asyncReducers, name)) return; 37 | 38 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 39 | store.replaceReducer(createReducer(store.asyncReducers)); 40 | }; 41 | } 42 | 43 | /** 44 | * Inject an asynchronously loaded saga 45 | */ 46 | export function injectAsyncSagas(store, isValid) { 47 | return function injectSagas(sagas) { 48 | if (!isValid) checkStore(store); 49 | 50 | invariant( 51 | Array.isArray(sagas), 52 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 53 | ); 54 | 55 | warning( 56 | !isEmpty(sagas), 57 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 58 | ); 59 | 60 | sagas.map(store.runSaga); 61 | }; 62 | } 63 | 64 | /** 65 | * Helper for creating injectors 66 | */ 67 | export function getAsyncInjectors(store) { 68 | checkStore(store); 69 | 70 | return { 71 | injectReducer: injectAsyncReducer(store, true), 72 | injectSagas: injectAsyncSagas(store, true), 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "mocha": true, 8 | "es6": true 9 | }, 10 | "plugins": [ 11 | "redux-saga", 12 | "react", 13 | "jsx-a11y" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 6, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | } 21 | }, 22 | "rules": { 23 | "arrow-parens": [ 24 | "error", 25 | "always" 26 | ], 27 | "arrow-body-style": [ 28 | 2, 29 | "as-needed" 30 | ], 31 | "comma-dangle": [ 32 | 2, 33 | "always-multiline" 34 | ], 35 | "import/imports-first": 0, 36 | "import/newline-after-import": 0, 37 | "import/no-dynamic-require": 0, 38 | "import/no-extraneous-dependencies": 0, 39 | "import/no-named-as-default": 0, 40 | "import/no-unresolved": 2, 41 | "import/prefer-default-export": 0, 42 | "indent": [ 43 | 2, 44 | 2, 45 | { 46 | "SwitchCase": 1 47 | } 48 | ], 49 | "jsx-a11y/aria-props": 2, 50 | "jsx-a11y/heading-has-content": 0, 51 | "jsx-a11y/href-no-hash": 2, 52 | "jsx-a11y/label-has-for": 2, 53 | "jsx-a11y/mouse-events-have-key-events": 2, 54 | "jsx-a11y/role-has-required-aria-props": 2, 55 | "jsx-a11y/role-supports-aria-props": 2, 56 | "max-len": 0, 57 | "newline-per-chained-call": 0, 58 | "no-console": 1, 59 | "no-use-before-define": 0, 60 | "prefer-template": 2, 61 | "class-methods-use-this": 0, 62 | "react/forbid-prop-types": 0, 63 | "react/jsx-first-prop-new-line": [ 64 | 2, 65 | "multiline" 66 | ], 67 | "react/jsx-filename-extension": 0, 68 | "react/jsx-no-target-blank": 0, 69 | "react/prefer-stateless-function": 0, 70 | "react/require-extension": 0, 71 | "react/self-closing-comp": 0, 72 | "redux-saga/no-yield-in-race": 2, 73 | "redux-saga/yield-effects": 2, 74 | "require-yield": 0 75 | }, 76 | "settings": { 77 | "import/resolver": { 78 | "webpack": { 79 | "config": "./internals/webpack/webpack.test.babel.js" 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /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 { 24 | mySelector, 25 | }; 26 | ``` 27 | 28 | ### Complex selectors 29 | 30 | If we need to, we can combine simple selectors to build more complex ones which 31 | get nested state parts with reselect's `createSelector` function. We import other 32 | selectors and pass them to the `createSelector` call: 33 | 34 | ```javascript 35 | import { createSelector } from 'reselect'; 36 | import mySelector from 'mySelector'; 37 | 38 | const myComplexSelector = createSelector( 39 | mySelector, 40 | (myState) => myState.get('someNestedState') 41 | ); 42 | 43 | export { 44 | myComplexSelector, 45 | }; 46 | ``` 47 | 48 | These selectors can then either be used directly in our containers as 49 | `mapStateToProps` functions or be nested with `createSelector` once again: 50 | 51 | ```javascript 52 | export default connect(createSelector( 53 | myComplexSelector, 54 | (myNestedState) => ({ data: myNestedState }) 55 | ))(SomeComponent); 56 | ``` 57 | 58 | ### Adding a new selector 59 | 60 | If you have a `selectors.js` file next to the reducer which's part of the state 61 | you want to select, add your selector to said file. If you don't have one yet, 62 | add a new one into your container folder and fill it with this boilerplate code: 63 | 64 | ```JS 65 | import { createSelector } from 'reselect'; 66 | 67 | const selectMyState = () => createSelector( 68 | 69 | ); 70 | 71 | export { 72 | selectMyState, 73 | }; 74 | ``` 75 | 76 | --- 77 | 78 | _Don't like this feature? [Click here](remove.md)_ 79 | -------------------------------------------------------------------------------- /internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | const fs = require('fs'); 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | function reducerExists(comp) { 8 | try { 9 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | 16 | function sagasExists(comp) { 17 | try { 18 | fs.accessSync(`app/containers/${comp}/sagas.js`, fs.F_OK); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | function trimTemplateFile(template) { 26 | // Loads the template file and trims the whitespace and then returns the content as a string. 27 | return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, ''); 28 | } 29 | 30 | module.exports = { 31 | description: 'Add a route', 32 | prompts: [{ 33 | type: 'input', 34 | name: 'component', 35 | message: 'Which component should the route show?', 36 | validate: (value) => { 37 | if ((/.+/).test(value)) { 38 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 39 | } 40 | 41 | return 'The path is required'; 42 | }, 43 | }, { 44 | type: 'input', 45 | name: 'path', 46 | message: 'Enter the path of the route.', 47 | default: '/about', 48 | validate: (value) => { 49 | if ((/.+/).test(value)) { 50 | return true; 51 | } 52 | 53 | return 'path is required'; 54 | }, 55 | }], 56 | 57 | // Add the route to the routes.js file above the error route 58 | // TODO smarter route adding 59 | actions: (data) => { 60 | const actions = []; 61 | if (reducerExists(data.component)) { 62 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign 63 | actions.push({ 64 | type: 'modify', 65 | path: '../../app/routes.js', 66 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 67 | template: trimTemplateFile('routeWithReducer.hbs'), 68 | }); 69 | } else { 70 | actions.push({ 71 | type: 'modify', 72 | path: '../../app/routes.js', 73 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 74 | template: trimTemplateFile('route.hbs'), 75 | }); 76 | } 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const OfflinePlugin = require('offline-plugin'); 6 | 7 | module.exports = require('./webpack.base.babel')({ 8 | // In production, we skip all hot-reloading stuff 9 | entry: [ 10 | path.join(process.cwd(), 'app/app.js'), 11 | ], 12 | 13 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 14 | output: { 15 | filename: '[name].[chunkhash].js', 16 | chunkFilename: '[name].[chunkhash].chunk.js', 17 | }, 18 | 19 | plugins: [ 20 | new webpack.optimize.CommonsChunkPlugin({ 21 | name: 'vendor', 22 | children: true, 23 | minChunks: 2, 24 | async: true, 25 | }), 26 | 27 | // Minify and optimize the index.html 28 | new HtmlWebpackPlugin({ 29 | template: 'app/index.html', 30 | minify: { 31 | removeComments: true, 32 | collapseWhitespace: true, 33 | removeRedundantAttributes: true, 34 | useShortDoctype: true, 35 | removeEmptyAttributes: true, 36 | removeStyleLinkTypeAttributes: true, 37 | keepClosingSlash: true, 38 | minifyJS: true, 39 | minifyCSS: true, 40 | minifyURLs: true, 41 | }, 42 | inject: true, 43 | }), 44 | 45 | // Put it in the end to capture all the HtmlWebpackPlugin's 46 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 47 | new OfflinePlugin({ 48 | relativePaths: false, 49 | publicPath: '/', 50 | 51 | // No need to cache .htaccess. See http://mxs.is/googmp, 52 | // this is applied before any match in `caches` section 53 | excludes: ['.htaccess'], 54 | 55 | caches: { 56 | main: [':rest:'], 57 | 58 | // All chunks marked as `additional`, loaded after main section 59 | // and do not prevent SW to install. Change to `optional` if 60 | // do not want them to be preloaded at all (cached only when first loaded) 61 | additional: ['*.chunk.js'], 62 | }, 63 | 64 | // Removes warning for about `additional` section usage 65 | safeToUseOptionalCaches: true, 66 | 67 | AppCache: false, 68 | }), 69 | ], 70 | }); 71 | -------------------------------------------------------------------------------- /internals/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const webpack = require('webpack'); 6 | const modules = [ 7 | 'app', 8 | 'node_modules', 9 | ]; 10 | 11 | module.exports = { 12 | devtool: 'inline-source-map', 13 | module: { 14 | // Some libraries don't like being run through babel. 15 | // If they gripe, put them here. 16 | noParse: [ 17 | /node_modules(\\|\/)sinon/, 18 | /node_modules(\\|\/)acorn/, 19 | ], 20 | loaders: [ 21 | { test: /\.json$/, loader: 'json-loader' }, 22 | { test: /\.css$/, loader: 'null-loader' }, 23 | 24 | // sinon.js--aliased for enzyme--expects/requires global vars. 25 | // imports-loader allows for global vars to be injected into the module. 26 | // See https://github.com/webpack/webpack/issues/304 27 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 28 | loader: 'imports?define=>false,require=>false', 29 | }, 30 | { test: /\.js$/, 31 | loader: 'babel', 32 | exclude: [/node_modules/], 33 | }, 34 | { test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i, 35 | loader: 'null-loader', 36 | }, 37 | ], 38 | }, 39 | 40 | plugins: [ 41 | 42 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 43 | // inside your code for any environment checks; UglifyJS will automatically 44 | // drop any unreachable code. 45 | new webpack.DefinePlugin({ 46 | 'process.env': { 47 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 48 | }, 49 | })], 50 | 51 | // Some node_modules pull in Node-specific dependencies. 52 | // Since we're running in a browser we have to stub them out. See: 53 | // https://webpack.github.io/docs/configuration.html#node 54 | // https://github.com/webpack/node-libs-browser/tree/master/mock 55 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 56 | node: { 57 | fs: 'empty', 58 | child_process: 'empty', 59 | net: 'empty', 60 | tls: 'empty', 61 | }, 62 | 63 | // required for enzyme to work properly 64 | externals: { 65 | jsdom: 'window', 66 | 'react/addons': true, 67 | 'react/lib/ExecutionEnvironment': true, 68 | 'react/lib/ReactContext': 'window', 69 | }, 70 | resolve: { 71 | modules, 72 | alias: { 73 | // required for enzyme to work properly 74 | sinon: 'sinon/pkg/sinon', 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add an unconnected component', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of component', 13 | default: 'ES6 Class (Pure)', 14 | choices: () => ['ES6 Class (Pure)', 'ES6 Class'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'Button', 20 | validate: (value) => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A component or container with this name already exists' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }, { 28 | type: 'confirm', 29 | name: 'wantMessages', 30 | default: false, 31 | message: 'Do you want i18n messages (i.e. will this component use text)?', 32 | }], 33 | actions: (data) => { 34 | // Generate index.js and index.test.js 35 | let componentTemplate; 36 | 37 | switch (data.type) { 38 | case 'ES6 Class': { 39 | componentTemplate = './component/es6.js.hbs'; 40 | break; 41 | } 42 | case 'ES6 Class (Pure)': { 43 | componentTemplate = './component/es6.pure.js.hbs'; 44 | break; 45 | } 46 | default: { 47 | componentTemplate = './component/es6.pure.js.hbs'; 48 | } 49 | } 50 | 51 | const actions = [{ 52 | type: 'add', 53 | path: '../../app/components/{{properCase name}}/index.js', 54 | templateFile: componentTemplate, 55 | abortOnFail: true, 56 | }, { 57 | type: 'add', 58 | path: '../../app/components/{{properCase name}}/view.js', 59 | templateFile: './component/view.js.hbs', 60 | abortOnFail: true, 61 | }, { 62 | type: 'add', 63 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 64 | templateFile: './component/test.js.hbs', 65 | abortOnFail: true, 66 | }]; 67 | 68 | // If they want a i18n messages file 69 | if (data.wantMessages) { 70 | actions.push({ 71 | type: 'add', 72 | path: '../../app/components/{{properCase name}}/messages.js', 73 | templateFile: './component/messages.js.hbs', 74 | abortOnFail: true, 75 | }); 76 | } 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /app/components/AutoComplete/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * AutoComplete 4 | * 5 | */ 6 | 7 | import { PropTypes, PureComponent } from 'react'; 8 | import view from './view'; 9 | 10 | class AutoComplete extends PureComponent { 11 | static propTypes = { 12 | anchorOrigin: PropTypes.object, 13 | animated: PropTypes.bool, 14 | animation: PropTypes.func, 15 | dataSource: PropTypes.array, 16 | dataSourceConfig: PropTypes.object, 17 | disableFocusRipple: PropTypes.bool, 18 | errorStyle: PropTypes.object, 19 | errorText: PropTypes.node, 20 | filter: PropTypes.func, 21 | floatingLabelText: PropTypes.node, 22 | fullWidth: PropTypes.bool, 23 | hintText: PropTypes.node, 24 | listStyle: PropTypes.object, 25 | maxSearchResults: PropTypes.number, 26 | menuCloseDelay: PropTypes.number, 27 | menuProps: PropTypes.object, 28 | menuStyle: PropTypes.object, 29 | onClose: PropTypes.func, 30 | onNewRequest: PropTypes.func, 31 | onUpdateInput: PropTypes.func, 32 | open: PropTypes.bool, 33 | openOnFocus: PropTypes.bool, 34 | popoverProps: PropTypes.object, 35 | searchText: PropTypes.string, 36 | style: PropTypes.object, 37 | targetOrigin: PropTypes.object, 38 | textFieldStyle: PropTypes.object, 39 | }; 40 | 41 | static defaultProps = { 42 | anchorOrigin: { 43 | vertical: 'bottom', 44 | horizontal: 'left', 45 | }, 46 | animated: true, 47 | animation: () => {}, 48 | dataSource: undefined, 49 | dataSourceConfig: { 50 | text: 'text', 51 | value: 'value', 52 | }, 53 | disableFocusRipple: true, 54 | errorStyle: undefined, 55 | errorText: undefined, 56 | filter: (searchText, key) => ( 57 | searchText !== '' && key.indexOf(searchText) !== -1 58 | ), 59 | floatingLabelText: undefined, 60 | fullWidth: false, 61 | hintText: undefined, 62 | listStyle: undefined, 63 | maxSearchResults: undefined, 64 | menuCloseDelay: 300, 65 | menuProps: undefined, 66 | menuStyle: undefined, 67 | onClose: () => {}, 68 | onNewRequest: () => {}, 69 | onUpdateInput: () => {}, 70 | open: false, 71 | openOnFocus: false, 72 | popoverProps: undefined, 73 | searchText: '', 74 | style: undefined, 75 | targetOrigin: { 76 | vertical: 'top', 77 | horizontal: 'left', 78 | }, 79 | textFieldStyle: undefined, 80 | }; 81 | } 82 | 83 | AutoComplete.prototype.render = view; 84 | 85 | export default AutoComplete; 86 | -------------------------------------------------------------------------------- /server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const compression = require('compression'); 5 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 6 | 7 | // Dev middleware 8 | const addDevMiddlewares = (app, webpackConfig) => { 9 | const webpack = require('webpack'); 10 | const webpackDevMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const compiler = webpack(webpackConfig); 13 | const middleware = webpackDevMiddleware(compiler, { 14 | noInfo: true, 15 | publicPath: webpackConfig.output.publicPath, 16 | silent: true, 17 | stats: 'errors-only', 18 | }); 19 | 20 | app.use(middleware); 21 | app.use(webpackHotMiddleware(compiler)); 22 | 23 | // Since webpackDevMiddleware uses memory-fs internally to store build 24 | // artifacts, we use it instead 25 | const fs = middleware.fileSystem; 26 | 27 | if (pkg.dllPlugin) { 28 | app.get(/\.dll\.js$/, (req, res) => { 29 | const filename = req.path.replace(/^\//, ''); 30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)); 31 | }); 32 | } 33 | 34 | app.get('*', (req, res) => { 35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 36 | if (err) { 37 | res.sendStatus(404); 38 | } else { 39 | res.send(file.toString()); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | // Production middlewares 46 | const addProdMiddlewares = (app, options) => { 47 | const publicPath = options.publicPath || '/'; 48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 49 | 50 | // compression middleware compresses your server responses which makes them 51 | // smaller (applies also to assets). You can read more about that technique 52 | // and other good practices on official Express.js docs http://mxs.is/googmy 53 | app.use(compression()); 54 | app.use(publicPath, express.static(outputPath)); 55 | 56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))); 57 | }; 58 | 59 | /** 60 | * Front-end middleware 61 | */ 62 | module.exports = (app, options) => { 63 | const isProd = process.env.NODE_ENV === 'production'; 64 | 65 | if (isProd) { 66 | addProdMiddlewares(app, options); 67 | } else { 68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 69 | addDevMiddlewares(app, webpackConfig); 70 | } 71 | 72 | return app; 73 | }; 74 | -------------------------------------------------------------------------------- /internals/webpack/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * COMMON WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = (options) => ({ 9 | entry: options.entry, 10 | output: Object.assign({ // Compile into js/build.js 11 | path: path.resolve(process.cwd(), 'build'), 12 | publicPath: '/', 13 | }, options.output), // Merge with env dependent settings 14 | module: { 15 | loaders: [{ 16 | test: /\.js$/, // Transform all .js files required somewhere with Babel 17 | loader: 'babel', 18 | exclude: /node_modules/, 19 | query: options.babelQuery, 20 | }, { 21 | // Do not transform vendor's CSS with CSS-modules 22 | // The point is that they remain in global scope. 23 | // Since we require these CSS files in our JS or CSS files, 24 | // they will be a part of our compilation either way. 25 | // So, no need for ExtractTextPlugin here. 26 | test: /\.css$/, 27 | include: /node_modules/, 28 | loaders: ['style-loader', 'css-loader'], 29 | }, { 30 | test: /\.(eot|svg|ttf|woff|woff2)$/, 31 | loader: 'file-loader', 32 | }, { 33 | test: /\.(jpg|png|gif)$/, 34 | loaders: [ 35 | 'file-loader', 36 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 37 | ], 38 | }, { 39 | test: /\.html$/, 40 | loader: 'html-loader', 41 | }, { 42 | test: /\.json$/, 43 | loader: 'json-loader', 44 | }, { 45 | test: /\.(mp4|webm)$/, 46 | loader: 'url-loader?limit=10000', 47 | }], 48 | }, 49 | plugins: options.plugins.concat([ 50 | new webpack.ProvidePlugin({ 51 | // make fetch available 52 | fetch: 'exports?self.fetch!whatwg-fetch', 53 | }), 54 | 55 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 56 | // inside your code for any environment checks; UglifyJS will automatically 57 | // drop any unreachable code. 58 | new webpack.DefinePlugin({ 59 | 'process.env': { 60 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 61 | }, 62 | }), 63 | new webpack.NamedModulesPlugin(), 64 | ]), 65 | resolve: { 66 | modules: ['app', 'node_modules'], 67 | extensions: [ 68 | '.js', 69 | '.jsx', 70 | '.react.js', 71 | ], 72 | mainFields: [ 73 | 'browser', 74 | 'jsnext:main', 75 | 'main', 76 | ], 77 | }, 78 | devtool: options.devtool, 79 | target: 'web', // Make web variables accessible to webpack, e.g. window 80 | }); 81 | -------------------------------------------------------------------------------- /internals/generators/language/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Generator 3 | */ 4 | const exec = require('child_process').exec; 5 | 6 | module.exports = { 7 | description: 'Add a language', 8 | prompts: [{ 9 | type: 'input', 10 | name: 'language', 11 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?', 12 | default: 'fr', 13 | validate: (value) => { 14 | if ((/.+/).test(value) && value.length === 2) { 15 | return true; 16 | } 17 | 18 | return '2 character language specifier is required'; 19 | }, 20 | }], 21 | 22 | actions: () => { 23 | const actions = []; 24 | actions.push({ 25 | type: 'modify', 26 | path: '../../app/i18n.js', 27 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g, 28 | templateFile: './language/intl-locale-data.hbs', 29 | }); 30 | actions.push({ 31 | type: 'modify', 32 | path: '../../app/i18n.js', 33 | pattern: /(\s+'[a-z]+',\n)(?!.*\s+'[a-z]+',)/g, 34 | templateFile: './language/app-locale.hbs', 35 | }); 36 | actions.push({ 37 | type: 'modify', 38 | path: '../../app/i18n.js', 39 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g, 40 | templateFile: './language/translation-messages.hbs', 41 | }); 42 | actions.push({ 43 | type: 'modify', 44 | path: '../../app/i18n.js', 45 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g, 46 | templateFile: './language/add-locale-data.hbs', 47 | }); 48 | actions.push({ 49 | type: 'modify', 50 | path: '../../app/i18n.js', 51 | pattern: /([a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),)/g, 52 | templateFile: './language/format-translation-messages.hbs', 53 | }); 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/translations/{{language}}.json', 57 | templateFile: './language/translations-json.hbs', 58 | abortOnFail: true, 59 | }); 60 | actions.push({ 61 | type: 'modify', 62 | path: '../../app/app.js', 63 | pattern: /(System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g, 64 | templateFile: './language/polyfill-intl-locale.hbs', 65 | }); 66 | actions.push( 67 | () => { 68 | const cmd = 'npm run extract-intl'; 69 | exec(cmd, (err, result, stderr) => { 70 | if (err || stderr) { 71 | throw err || stderr; 72 | } 73 | process.stdout.write(result); 74 | }); 75 | } 76 | ); 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /docs/js/redux-saga.md: -------------------------------------------------------------------------------- 1 | # `redux-saga` 2 | 3 | `redux-saga` is a library to manage side effects in your application. It works 4 | beautifully for data fetching, concurrent computations and a lot more. 5 | [Sebastien Lorber](https://twitter.com/sebastienlorber) put it best: 6 | 7 | > Imagine there is widget1 and widget2. When some button on widget1 is clicked, 8 | then it should have an effect on widget2. Instead of coupling the 2 widgets 9 | together (ie widget1 dispatch an action that targets widget2), widget1 only 10 | dispatch that its button was clicked. Then the saga listen for this button 11 | click and then update widget2 by dispatching a new event that widget2 is aware of. 12 | > 13 | > This adds a level of indirection that is unnecessary for simple apps, but make 14 | it more easy to scale complex applications. You can now publish widget1 and 15 | widget2 to different npm repositories so that they never have to know about 16 | each others, without having them to share a global registry of actions. The 2 17 | widgets are now bounded contexts that can live separately. They do not need 18 | each others to be consistent and can be reused in other apps as well. **The saga 19 | is the coupling point between the two widgets that coordinate them in a 20 | meaningful way for your business.** 21 | 22 | _Note: It is well worth reading the [source](https://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34623840#34623840) 23 | of this quote in its entirety!_ 24 | 25 | To learn more about this amazing way to handle concurrent flows, start with the 26 | [official documentation](https://github.com/yelouafi/redux-saga) and explore 27 | some examples! (read [this comparison](https://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es7-async/34933395) if you're used to `redux-thunk`) 28 | 29 | ## Usage 30 | 31 | Sagas are associated with a container, just like actions, constants, selectors 32 | and reducers. If your container already has a `sagas.js` file, simply add your 33 | saga to that. If your container does not yet have a `sagas.js` file, add one with 34 | this boilerplate structure: 35 | 36 | ```JS 37 | import { take, call, put, select } from 'redux-saga/effects'; 38 | 39 | // Your sagas for this container 40 | export default [ 41 | sagaName, 42 | ]; 43 | 44 | // Individual exports for testing 45 | export function* sagaName() { 46 | 47 | } 48 | ``` 49 | 50 | Then, in your `routes.js`, add injection for the newly added saga: 51 | 52 | ```JS 53 | getComponent(nextState, cb) { 54 | const importModules = Promise.all([ 55 | System.import('containers/YourComponent/reducer'), 56 | System.import('containers/YourComponent/sagas'), 57 | System.import('containers/YourComponent'), 58 | ]); 59 | 60 | const renderRoute = loadModule(cb); 61 | 62 | importModules.then(([reducer, sagas, component]) => { 63 | injectReducer('home', reducer.default); 64 | injectSagas(sagas.default); // Inject the saga 65 | 66 | renderRoute(component); 67 | }); 68 | 69 | importModules.catch(errorLoading); 70 | }, 71 | ``` 72 | 73 | Now add as many sagas to your `sagas.js` file as you want! 74 | 75 | --- 76 | 77 | _Don't like this feature? [Click here](remove.md)_ 78 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Table of Contents 4 | 5 | - [General](general) 6 | - [**CLI Commands**](general/commands.md) 7 | - [Tool Configuration](general/files.md) 8 | - [Server Configurations](general/server-configs.md) 9 | - [Deployment](general/deployment.md) *(currently Heroku specific)* 10 | - [FAQ](general/faq.md) 11 | - [Gotchas](general/gotchas.md) 12 | - [Remove](general/remove.md) 13 | - [Testing](testing) 14 | - [Unit Testing](testing/unit-testing.md) 15 | - [Component Testing](testing/component-testing.md) 16 | - [Remote Testing](testing/remote-testing.md) 17 | - [CSS](css) 18 | - [`styled-components`](css/styled-componets.md) 19 | - [sanitize.css](css/sanitize.md) 20 | - [JS](js) 21 | - [Redux](js/redux.md) 22 | - [ImmutableJS](js/immutablejs.md) 23 | - [reselect](js/reselect.md) 24 | - [redux-saga](js/redux-saga.md) 25 | - [i18n](js/i18n.md) 26 | - [routing](js/routing.md) 27 | 28 | ## Overview 29 | 30 | ### Development 31 | 32 | Run `npm start` to see your app at `localhost:3000` 33 | 34 | ### Building & Deploying 35 | 36 | 1. Run `npm run build`, which will compile all the necessary files to the 37 | `build` folder. 38 | 39 | 2. Upload the contents of the `build` folder to your web server's root folder. 40 | 41 | ### Structure 42 | 43 | The [`app/`](../../../tree/master/app) directory contains your entire application code, including CSS, 44 | JavaScript, HTML and tests. 45 | 46 | The rest of the folders and files only exist to make your life easier, and 47 | should not need to be touched. 48 | 49 | *(If they do have to be changed, please [submit an issue](https://github.com/kelsonic/react-redux-material-ui-boilerplate/issues)!)* 50 | 51 | ### CSS 52 | 53 | Utilising [tagged template literals](./docs/tagged-template-literals.md) 54 | (a recent addition to JavaScript) and the [power of CSS](./docs/css-we-support.md), 55 | `styled-components` allows you to write actual CSS code to style your components. 56 | It also removes the mapping between components and styles – using components as a 57 | low-level styling construct could not be easier! 58 | 59 | See the [CSS documentation](./css/README.md) for more information. 60 | 61 | ### JS 62 | 63 | We bundle all your clientside scripts and chunk them into several files using 64 | code splitting where possible. We then automatically optimize your code when 65 | building for production so you don't have to worry about that. 66 | 67 | See the [JS documentation](./js/README.md) for more information about the 68 | JavaScript side of things. 69 | 70 | ### SEO 71 | 72 | We use [react-helmet](https://github.com/nfl/react-helmet) for managing document head tags. Examples on how to 73 | write head tags can be found [here](https://github.com/nfl/react-helmet#examples). 74 | 75 | ### Testing 76 | 77 | For a thorough explanation of the testing procedure, see the 78 | [testing documentation](./testing/README.md)! 79 | 80 | #### Performance testing 81 | 82 | With the production server running (i.e. while `npm run start:production` is running in 83 | another tab), enter `npm run pagespeed` to run Google PageSpeed Insights and 84 | get a performance check right in your terminal! 85 | 86 | #### Browser testing 87 | 88 | `npm run start:tunnel` makes your locally-running app globally available on the web 89 | via a temporary URL: great for testing on different devices, client demos, etc! 90 | 91 | #### Unit testing 92 | 93 | Unit tests live in `test/` directories right next to the components being tested 94 | and are run with `npm run test`. 95 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | import 'babel-polyfill'; 8 | 9 | /* eslint-disable import/no-unresolved, import/extensions */ 10 | // Load the manifest.json file and the .htaccess file 11 | import '!file?name=[name].[ext]!./manifest.json'; 12 | import 'file?name=[name].[ext]!./.htaccess'; 13 | /* eslint-enable import/no-unresolved, import/extensions */ 14 | 15 | // Import all the third party stuff 16 | import React from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import { Provider } from 'react-redux'; 19 | import { applyRouterMiddleware, Router, browserHistory } from 'react-router'; 20 | import { syncHistoryWithStore } from 'react-router-redux'; 21 | import { useScroll } from 'react-router-scroll'; 22 | import LanguageProvider from 'containers/LanguageProvider'; 23 | import configureStore from './store'; 24 | 25 | // Import i18n messages 26 | import { translationMessages } from './i18n'; 27 | 28 | // Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder 29 | import 'sanitize.css/sanitize.css'; 30 | 31 | // Needed for onTouchTap 32 | // http://stackoverflow.com/a/34015469/988941 33 | import injectTapEventPlugin from 'react-tap-event-plugin'; 34 | injectTapEventPlugin(); 35 | 36 | // Create redux store with history 37 | // this uses the singleton browserHistory provided by react-router 38 | // Optionally, this could be changed to leverage a created history 39 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 40 | const initialState = {}; 41 | const store = configureStore(initialState, browserHistory); 42 | 43 | // Sync history and store, as the react-router-redux reducer 44 | // is under the non-default key ("routing"), selectLocationState 45 | // must be provided for resolving how to retrieve the "route" in the state 46 | import { selectLocationState } from 'containers/App/selectors'; 47 | const history = syncHistoryWithStore(browserHistory, store, { 48 | selectLocationState: selectLocationState(), 49 | }); 50 | 51 | // Set up the router, wrapping all Routes in the App component 52 | import App from 'containers/App'; 53 | import createRoutes from './routes'; 54 | const rootRoute = { 55 | component: App, 56 | childRoutes: createRoutes(store), 57 | }; 58 | 59 | 60 | const render = (translatedMessages) => { 61 | ReactDOM.render( 62 | 63 | 64 | 73 | 74 | , 75 | document.getElementById('app') 76 | ); 77 | }; 78 | 79 | 80 | // Hot reloadable translation json files 81 | if (module.hot) { 82 | // modules.hot.accept does not accept dynamic dependencies, 83 | // have to be constants at compile-time 84 | module.hot.accept('./i18n', () => { 85 | render(translationMessages); 86 | }); 87 | } 88 | 89 | // Chunked polyfill for browsers without Intl support 90 | if (!window.Intl) { 91 | (new Promise((resolve) => { 92 | resolve(System.import('intl')); 93 | })) 94 | .then(() => Promise.all([ 95 | System.import('intl/locale-data/jsonp/de.js'), 96 | ])) 97 | .then(() => render(translationMessages)) 98 | .catch((err) => { 99 | throw err; 100 | }); 101 | } else { 102 | render(translationMessages); 103 | } 104 | 105 | // Install ServiceWorker and AppCache in the end since 106 | // it's not most important operation and if main code fails, 107 | // we do not want it installed 108 | import { install } from 'offline-plugin/runtime'; 109 | install(); 110 | -------------------------------------------------------------------------------- /docs/general/commands.md: -------------------------------------------------------------------------------- 1 | # Command Line Commands 2 | 3 | ## Development 4 | 5 | ```Shell 6 | npm run start 7 | ``` 8 | 9 | Starts the development server running on `http://localhost:3000` 10 | 11 | ## Generators 12 | 13 | ```Shell 14 | npm run generate 15 | ``` 16 | 17 | Allows you to auto-generate boilerplate code for common parts of your 18 | application, specifically `component`s, `container`s, and `route`s. You can 19 | also run `npm run generate ` to skip the first selection. (e.g. `npm run 20 | generate container`) 21 | 22 | ## Server 23 | 24 | ### Development 25 | 26 | ```Shell 27 | npm start 28 | ``` 29 | 30 | Starts the development server and makes your application accessible at 31 | `localhost:3000`. Tunnels that server with `ngrok`, which means the website 32 | accessible anywhere! Changes in the application code will be hot-reloaded. 33 | 34 | ### Production 35 | 36 | ```Shell 37 | npm run start:prod 38 | ``` 39 | 40 | Starts the production server, configured for optimal performance: assets are 41 | minified and served gzipped. 42 | 43 | ### Port 44 | 45 | To change the port the app is accessible at pass the `--port` option to the command 46 | with `--`. E.g. to make the app visible at `localhost:5000`, run the following: 47 | `npm start -- --port 5000` 48 | 49 | ## Building 50 | 51 | ```Shell 52 | npm run build 53 | ``` 54 | 55 | Preps your app for deployment. Optimizes and minifies all files, piping them to 56 | a folder called `build`. Upload the contents of `build` to your web server to 57 | see your work live! 58 | 59 | ## Testing 60 | 61 | See the [testing documentation](../testing/README.md) for detailed information 62 | about our testing setup! 63 | 64 | ## Unit testing 65 | 66 | ```Shell 67 | npm run test 68 | ``` 69 | 70 | Tests your application with the unit tests specified in the `*test.js` files 71 | throughout the application. 72 | All the `test` commands allow an optional `-- --grep string` argument to filter 73 | the tests ran by Karma. Useful if you need to run a specific test only. 74 | 75 | ```Shell 76 | # Run only the Button component tests 77 | npm run test:watch -- --grep Button 78 | ``` 79 | 80 | ### Browsers 81 | 82 | To choose the browser to run your unit tests in (Chrome by default), run one of 83 | the following commands: 84 | 85 | #### Firefox 86 | 87 | ```Shell 88 | npm run test:firefox 89 | ``` 90 | 91 | #### Safari 92 | 93 | ```Shell 94 | npm run test:safari 95 | ``` 96 | 97 | #### Internet Explorer 98 | 99 | *Windows only!* 100 | 101 | ```Shell 102 | npm run test:ie 103 | ``` 104 | 105 | ### Watching 106 | 107 | ```Shell 108 | npm run test:watch 109 | ``` 110 | 111 | Watches changes to your application and reruns tests whenever a file changes. 112 | 113 | ### Remote testing 114 | 115 | ```Shell 116 | npm run start:tunnel 117 | ``` 118 | Starts the development server and tunnels it with `ngrok`, making the website 119 | available on the entire world. Useful for testing on different devices in different locations! 120 | 121 | ### Performance testing 122 | 123 | ```Shell 124 | npm run pagespeed 125 | ``` 126 | 127 | With the remote server running (i.e. while `npm run start:prod` is running in 128 | another terminal session), enter this command to run Google PageSpeed Insights 129 | and get a performance check right in your terminal! 130 | 131 | ### Dependency size test 132 | 133 | ```Shell 134 | npm run analyze 135 | ``` 136 | 137 | This command will generate a `stats.json` file from your production build, which 138 | you can upload to the [webpack analyzer](https://webpack.github.io/analyse/). This 139 | analyzer will visualize your dependencies and chunks with detailed statistics 140 | about the bundle size. 141 | 142 | ## Linting 143 | 144 | ```Shell 145 | npm run lint 146 | ``` 147 | 148 | Lints your JavaScript and CSS. 149 | 150 | ### JavaScript 151 | 152 | ```Shell 153 | npm run lint:js 154 | ``` 155 | 156 | Only lints your JavaScript. 157 | 158 | ### CSS 159 | 160 | ```Shell 161 | npm run lint:css 162 | ``` 163 | 164 | Only lints your CSS. 165 | -------------------------------------------------------------------------------- /docs/js/i18n.md: -------------------------------------------------------------------------------- 1 | # `i18n` 2 | 3 | `react-intl` is a library to manage internationalization and pluralization support 4 | for your react application. This involves multi-language support for both the static text but also things like variable numbers, words or names that change with application state. `react-intl` provides an incredible amount of mature facility to preform these very tasks. 5 | 6 | The complete `react-intl` docs can be found here: 7 | 8 | https://github.com/yahoo/react-intl/wiki 9 | 10 | ## Usage 11 | 12 | Below we see a `messages.js` file for the `Footer` component example. A `messages.js` file should be included in any simple or container component that wants to use internationalization. You can add this support when you scaffold your component using this boilerplates scaffolding `plop` system. 13 | 14 | All default English text for the component is contained here (e.g. `This project is licensed under the MIT license.`), and is tagged with an ID (e.g. `boilerplate.components.Footer.license.message`) in addition to it's object definition id (e.g. `licenseMessage`). 15 | 16 | This is set in `react-intl`'s `defineMessages` function which is then exported for use in the component. You can read more about `defineMessages` here: 17 | 18 | https://github.com/yahoo/react-intl/wiki/API#definemessages 19 | 20 | ```js 21 | /* 22 | * Footer Messages 23 | * 24 | * This contains all the text for the Footer component. 25 | */ 26 | import { defineMessages } from 'react-intl'; 27 | 28 | export default defineMessages({ 29 | licenseMessage: { 30 | id: 'boilerplate.components.Footer.license.message', 31 | defaultMessage: 'This project is licensed under the MIT license.', 32 | }, 33 | authorMessage: { 34 | id: 'boilerplate.components.Footer.author.message', 35 | defaultMessage: ` 36 | Made with love by {author}. 37 | `, 38 | }, 39 | }); 40 | ``` 41 | 42 | Below is the example `Footer` component. Here we see the component including the `messages.js` file, which contains all the default component text, organized with ids (and optionally descriptions). We are also importing the `FormattedMessage` component, which will display a given message from the `messages.js` file in the selected language. 43 | 44 | You will also notice a more complex use of `FormattedMessage` for the author message where alternate or variable values (i.e. `author: Kelsonic,`) are being injected, in this case it's a react component. 45 | 46 | ```js 47 | import React from 'react'; 48 | 49 | import messages from './messages'; 50 | import A from 'components/A'; 51 | import styles from './styles.css'; 52 | import { FormattedMessage } from 'react-intl'; 53 | 54 | function Footer() { 55 | return ( 56 |
57 |
58 |

59 | 60 |

61 |
62 |
63 |

64 | Kelsonic, 68 | }} 69 | /> 70 |

71 |
72 |
73 | ); 74 | } 75 | 76 | export default Footer; 77 | ``` 78 | 79 | ## Extracting i18n JSON files 80 | 81 | You can extract all i18n language within each component by running the following command: 82 | 83 | ``` 84 | npm run extract-intl 85 | ``` 86 | 87 | This will extract all language into i18n JSON files in `app/translations`. 88 | 89 | ## Adding A Language 90 | 91 | You can add a language by running the generate command: 92 | 93 | ``` 94 | npm run generate language 95 | ``` 96 | 97 | Then enter the two character i18n standard language specifier (e.g. "fr", "de", "es" - without quotes). This will add in the necessary JSON language file and import statements for the language. Note, it is up to you to fill in the translations for the language. 98 | 99 | ## Removing i18n and react-intl 100 | 101 | You can remove `react-intl` modules by first removing the `IntlProvider` object from the `app/app.js` file and by either removing or not selecting the i18n text option during component scaffolding. 102 | 103 | The packages associated with `react-intl` are: 104 | - react-intl 105 | - babel-plugin-react-intl 106 | -------------------------------------------------------------------------------- /app/utils/tests/asyncInjectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test async injectors 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from 'store'; 7 | import { memoryHistory } from 'react-router'; 8 | import { put } from 'redux-saga/effects'; 9 | import { fromJS } from 'immutable'; 10 | 11 | import { 12 | injectAsyncReducer, 13 | injectAsyncSagas, 14 | getAsyncInjectors, 15 | } from 'utils/asyncInjectors'; 16 | 17 | // Fixtures 18 | 19 | const initialState = fromJS({ reduced: 'soon' }); 20 | 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'TEST': 24 | return state.set('reduced', action.payload); 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | function* testSaga() { 31 | yield put({ type: 'TEST', payload: 'yup' }); 32 | } 33 | 34 | const sagas = [ 35 | testSaga, 36 | ]; 37 | 38 | describe('asyncInjectors', () => { 39 | let store; 40 | 41 | describe('getAsyncInjectors', () => { 42 | before(() => { 43 | store = configureStore({}, memoryHistory); 44 | }); 45 | 46 | it('given a store, should return all async injectors', () => { 47 | const { injectReducer, injectSagas } = getAsyncInjectors(store); 48 | 49 | injectReducer('test', reducer); 50 | injectSagas(sagas); 51 | 52 | const actual = store.getState().get('test'); 53 | const expected = initialState.merge({ reduced: 'yup' }); 54 | 55 | expect(actual.toJS()).toEqual(expected.toJS()); 56 | }); 57 | 58 | it('should throw if passed invalid store shape', () => { 59 | let result = false; 60 | 61 | Reflect.deleteProperty(store, 'dispatch'); 62 | 63 | try { 64 | getAsyncInjectors(store); 65 | } catch (err) { 66 | result = err.name === 'Invariant Violation'; 67 | } 68 | 69 | expect(result).toEqual(true); 70 | }); 71 | }); 72 | 73 | describe('helpers', () => { 74 | before(() => { 75 | store = configureStore({}, memoryHistory); 76 | }); 77 | 78 | describe('injectAsyncReducer', () => { 79 | it('given a store, it should provide a function to inject a reducer', () => { 80 | const injectReducer = injectAsyncReducer(store); 81 | 82 | injectReducer('test', reducer); 83 | 84 | const actual = store.getState().get('test'); 85 | const expected = initialState; 86 | 87 | expect(actual.toJS()).toEqual(expected.toJS()); 88 | }); 89 | 90 | it('should throw if passed invalid name', () => { 91 | let result = false; 92 | 93 | const injectReducer = injectAsyncReducer(store); 94 | 95 | try { 96 | injectReducer('', reducer); 97 | } catch (err) { 98 | result = err.name === 'Invariant Violation'; 99 | } 100 | 101 | try { 102 | injectReducer(999, reducer); 103 | } catch (err) { 104 | result = err.name === 'Invariant Violation'; 105 | } 106 | 107 | expect(result).toEqual(true); 108 | }); 109 | 110 | it('should throw if passed invalid reducer', () => { 111 | let result = false; 112 | 113 | const injectReducer = injectAsyncReducer(store); 114 | 115 | try { 116 | injectReducer('bad', 'nope'); 117 | } catch (err) { 118 | result = err.name === 'Invariant Violation'; 119 | } 120 | 121 | try { 122 | injectReducer('coolio', 12345); 123 | } catch (err) { 124 | result = err.name === 'Invariant Violation'; 125 | } 126 | 127 | expect(result).toEqual(true); 128 | }); 129 | }); 130 | 131 | describe('injectAsyncSagas', () => { 132 | it('given a store, it should provide a function to inject a saga', () => { 133 | const injectSagas = injectAsyncSagas(store); 134 | 135 | injectSagas(sagas); 136 | 137 | const actual = store.getState().get('test'); 138 | const expected = initialState.merge({ reduced: 'yup' }); 139 | 140 | expect(actual.toJS()).toEqual(expected.toJS()); 141 | }); 142 | 143 | it('should throw if passed invalid saga', () => { 144 | let result = false; 145 | 146 | const injectSagas = injectAsyncSagas(store); 147 | 148 | try { 149 | injectSagas({ testSaga }); 150 | } catch (err) { 151 | result = err.name === 'Invariant Violation'; 152 | } 153 | 154 | try { 155 | injectSagas(testSaga); 156 | } catch (err) { 157 | result = err.name === 'Invariant Violation'; 158 | } 159 | 160 | expect(result).toEqual(true); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /internals/webpack/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DEVELOPMENT WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const webpack = require('webpack'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const logger = require('../../server/logger'); 10 | const cheerio = require('cheerio'); 11 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 12 | const dllPlugin = pkg.dllPlugin; 13 | 14 | const plugins = [ 15 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading 16 | new webpack.NoErrorsPlugin(), 17 | new HtmlWebpackPlugin({ 18 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js 19 | templateContent: templateContent(), // eslint-disable-line no-use-before-define 20 | }), 21 | ]; 22 | 23 | module.exports = require('./webpack.base.babel')({ 24 | // Add hot reloading in development 25 | entry: [ 26 | 'eventsource-polyfill', // Necessary for hot reloading with IE 27 | 'webpack-hot-middleware/client', 28 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js 29 | ], 30 | 31 | // Don't use hashes in dev mode for better performance 32 | output: { 33 | filename: '[name].js', 34 | chunkFilename: '[name].chunk.js', 35 | }, 36 | 37 | // Add development plugins 38 | plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define 39 | 40 | // Tell babel that we want to hot-reload 41 | babelQuery: { 42 | presets: ['react-hmre'], 43 | }, 44 | 45 | // Emit a source map for easier debugging 46 | devtool: 'cheap-module-eval-source-map', 47 | }); 48 | 49 | /** 50 | * Select which plugins to use to optimize the bundle's handling of 51 | * third party dependencies. 52 | * 53 | * If there is a dllPlugin key on the project's package.json, the 54 | * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin 55 | * will be used. 56 | * 57 | */ 58 | function dependencyHandlers() { 59 | // Don't do anything during the DLL Build step 60 | if (process.env.BUILDING_DLL) { return []; } 61 | 62 | // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin 63 | if (!dllPlugin) { 64 | return [ 65 | new webpack.optimize.CommonsChunkPlugin({ 66 | name: 'vendor', 67 | children: true, 68 | minChunks: 2, 69 | async: true, 70 | }), 71 | ]; 72 | } 73 | 74 | const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls'); 75 | 76 | /** 77 | * If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json 78 | * Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude 79 | * 80 | * @see https://github.com/kelsonic/react-redux-material-ui-boilerplate/tree/master/docs 81 | */ 82 | if (!dllPlugin.dlls) { 83 | const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json'); 84 | 85 | if (!fs.existsSync(manifestPath)) { 86 | logger.error('The DLL manifest is missing. Please run `npm run build:dll`'); 87 | process.exit(0); 88 | } 89 | 90 | return [ 91 | new webpack.DllReferencePlugin({ 92 | context: process.cwd(), 93 | manifest: require(manifestPath), // eslint-disable-line global-require 94 | }), 95 | ]; 96 | } 97 | 98 | // If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them. 99 | const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`)); 100 | 101 | return dllManifests.map((manifestPath) => { 102 | if (!fs.existsSync(path)) { 103 | if (!fs.existsSync(manifestPath)) { 104 | logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`); 105 | logger.error(`Expected to find it in ${dllPath}`); 106 | logger.error('Please run: npm run build:dll'); 107 | 108 | process.exit(0); 109 | } 110 | } 111 | 112 | return new webpack.DllReferencePlugin({ 113 | context: process.cwd(), 114 | manifest: require(manifestPath), // eslint-disable-line global-require 115 | }); 116 | }); 117 | } 118 | 119 | /** 120 | * We dynamically generate the HTML content in development so that the different 121 | * DLL Javascript files are loaded in script tags and available to our application. 122 | */ 123 | function templateContent() { 124 | const html = fs.readFileSync( 125 | path.resolve(process.cwd(), 'app/index.html') 126 | ).toString(); 127 | 128 | if (!dllPlugin) { return html; } 129 | 130 | const doc = cheerio(html); 131 | const body = doc.find('body'); 132 | const dllNames = !dllPlugin.dlls ? ['reactBoilerplateDeps'] : Object.keys(dllPlugin.dlls); 133 | 134 | dllNames.forEach((dllName) => body.append(``)); 135 | 136 | return doc.toString(); 137 | } 138 | -------------------------------------------------------------------------------- /internals/generators/container/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add a container component', 9 | prompts: [{ 10 | type: 'input', 11 | name: 'name', 12 | message: 'What should it be called?', 13 | default: 'Form', 14 | validate: (value) => { 15 | if ((/.+/).test(value)) { 16 | return componentExists(value) ? 'A component or container with this name already exists' : true; 17 | } 18 | 19 | return 'The name is required'; 20 | }, 21 | }, { 22 | type: 'list', 23 | name: 'component', 24 | message: 'Select a base component:', 25 | default: 'PureComponent', 26 | choices: () => ['PureComponent', 'Component'], 27 | }, { 28 | type: 'confirm', 29 | name: 'wantHeaders', 30 | default: false, 31 | message: 'Do you want headers?', 32 | }, { 33 | type: 'confirm', 34 | name: 'wantActionsAndReducer', 35 | default: true, 36 | message: 'Do you want an actions/constants/selectors/reducer tupel for this container?', 37 | }, { 38 | type: 'confirm', 39 | name: 'wantSagas', 40 | default: true, 41 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)', 42 | }, { 43 | type: 'confirm', 44 | name: 'wantMessages', 45 | default: true, 46 | message: 'Do you want i18n messages (i.e. will this component use text)?', 47 | }], 48 | actions: (data) => { 49 | // Generate index.js and index.test.js 50 | const actions = [{ 51 | type: 'add', 52 | path: '../../app/containers/{{properCase name}}/index.js', 53 | templateFile: './container/index.js.hbs', 54 | abortOnFail: true, 55 | }, { 56 | type: 'add', 57 | path: '../../app/containers/{{properCase name}}/view.js', 58 | templateFile: './container/view.js.hbs', 59 | abortOnFail: true, 60 | }, { 61 | type: 'add', 62 | path: '../../app/containers/{{properCase name}}/tests/index.test.js', 63 | templateFile: './container/test.js.hbs', 64 | abortOnFail: true, 65 | }]; 66 | 67 | // If component wants messages 68 | if (data.wantMessages) { 69 | actions.push({ 70 | type: 'add', 71 | path: '../../app/containers/{{properCase name}}/messages.js', 72 | templateFile: './container/messages.js.hbs', 73 | abortOnFail: true, 74 | }); 75 | } 76 | 77 | // If they want actions and a reducer, generate actions.js, constants.js, 78 | // reducer.js and the corresponding tests for actions and the reducer 79 | if (data.wantActionsAndReducer) { 80 | // Actions 81 | actions.push({ 82 | type: 'add', 83 | path: '../../app/containers/{{properCase name}}/actions.js', 84 | templateFile: './container/actions.js.hbs', 85 | abortOnFail: true, 86 | }); 87 | actions.push({ 88 | type: 'add', 89 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js', 90 | templateFile: './container/actions.test.js.hbs', 91 | abortOnFail: true, 92 | }); 93 | 94 | // Constants 95 | actions.push({ 96 | type: 'add', 97 | path: '../../app/containers/{{properCase name}}/constants.js', 98 | templateFile: './container/constants.js.hbs', 99 | abortOnFail: true, 100 | }); 101 | 102 | // Selectors 103 | actions.push({ 104 | type: 'add', 105 | path: '../../app/containers/{{properCase name}}/selectors.js', 106 | templateFile: './container/selectors.js.hbs', 107 | abortOnFail: true, 108 | }); 109 | actions.push({ 110 | type: 'add', 111 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js', 112 | templateFile: './container/selectors.test.js.hbs', 113 | abortOnFail: true, 114 | }); 115 | 116 | // Reducer 117 | actions.push({ 118 | type: 'add', 119 | path: '../../app/containers/{{properCase name}}/reducer.js', 120 | templateFile: './container/reducer.js.hbs', 121 | abortOnFail: true, 122 | }); 123 | actions.push({ 124 | type: 'add', 125 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js', 126 | templateFile: './container/reducer.test.js.hbs', 127 | abortOnFail: true, 128 | }); 129 | } 130 | 131 | // Sagas 132 | if (data.wantSagas) { 133 | actions.push({ 134 | type: 'add', 135 | path: '../../app/containers/{{properCase name}}/sagas.js', 136 | templateFile: './container/sagas.js.hbs', 137 | abortOnFail: true, 138 | }); 139 | actions.push({ 140 | type: 'add', 141 | path: '../../app/containers/{{properCase name}}/tests/sagas.test.js', 142 | templateFile: './container/sagas.test.js.hbs', 143 | abortOnFail: true, 144 | }); 145 | } 146 | 147 | return actions; 148 | }, 149 | }; 150 | -------------------------------------------------------------------------------- /docs/js/immutablejs.md: -------------------------------------------------------------------------------- 1 | # ImmutableJS 2 | 3 | Immutable data structures can be deeply compared in no time. This allows us to 4 | efficiently determine if our components need to rerender since we know if the 5 | `props` changed or not! 6 | 7 | Check out the [official documentation](https://facebook.github.io/immutable-js/) 8 | for a good explanation of the more intricate benefits it has. 9 | 10 | ## Usage 11 | 12 | In our reducers, we make the initial state an immutable data structure with the 13 | `fromJS` function. We pass it an object or an array, and it takes care of 14 | converting it to a immutable data structure. (Note: the conversion is performed deeply so 15 | that even arbitrarily nested arrays/objects are immutable structures too!) 16 | 17 | ```JS 18 | import { fromJS } from 'immutable'; 19 | 20 | const initialState = fromJS({ 21 | myData: { 22 | message: 'Hello World!' 23 | }, 24 | }); 25 | ``` 26 | 27 | 28 | 29 | When a reducer is subscribed to an action and needs to return the new state they can do so by using setter methods such as [`.set`](https://facebook.github.io/immutable-js/docs/#/Map/set) and [`.update`](https://facebook.github.io/immutable-js/docs/#/Map/update) and [`.merge`](https://facebook.github.io/immutable-js/docs/#/Map/merge). 30 | If the changing state data is nested, we can utilize the 'deep' versions of these setters: [`.setIn`](https://facebook.github.io/immutable-js/docs/#/Map/setIn) and [`.updateIn`](https://facebook.github.io/immutable-js/docs/#/Map/updateIn), [`.mergeIn`](https://facebook.github.io/immutable-js/docs/#/Map/mergeIn). 31 | 32 | ```JS 33 | import { SOME_ACTION, SOME_OTHER_ACTION } from './actions'; 34 | 35 | // […] 36 | 37 | function myReducer(state = initialState, action) { 38 | switch (action.type) { 39 | case SOME_ACTION: 40 | return state.set('myData', action.payload); 41 | case SOME_OTHER_ACTION: 42 | return state.setIn(['myData', 'message'], action.payload); 43 | default: 44 | return state; 45 | } 46 | } 47 | ``` 48 | 49 | We use [`reselect`](./reselect.md) to efficiently cache our computed application 50 | state. Since that state is now immutable, we need to use the [`.get`](https://facebook.github.io/immutable-js/docs/#/Iterable/get) and [`.getIn`](https://facebook.github.io/immutable-js/docs/#/Iterable/getIn) 51 | functions to select the part we want. 52 | 53 | ```JS 54 | const myDataSelector = (state) => state.get('myData'); 55 | const messageSelector = (state) => state.getIn(['myData', 'message']); 56 | 57 | export default myDataSelector; 58 | ``` 59 | 60 | To learn more, check out [`reselect.md`](reselect.md)! 61 | 62 | ## Immutable Records 63 | 64 | ImmutableJS provides a number of immutable structures such as [`Map`](https://facebook.github.io/immutable-js/docs/#/Map), [`Set`](https://facebook.github.io/immutable-js/docs/#/Set) and [`List`](https://facebook.github.io/immutable-js/docs/#/List). 65 | One drawback to these structures is that properties must be accessed via the getter methods (`.get` or `.getIn`) and cannot be accessed with dot notation as they would in a plain javascript object. 66 | For instance you'll write `map.get('property')` instead of `object.property`, and `list.get(0)` instead of `array[0]`. 67 | This can make your code a little harder to follow and requires you to be extra cautious when passing arguments or props to functions or components that try to access values with regular dot notation. 68 | ImmutableJS's [`Record`](https://facebook.github.io/immutable-js/docs/#/Record) structure offers a solution to this issue. 69 | 70 | A `Record` is similar to a `Map` but has a fixed shape, meaning it's property keys are predefined and you can't later add a new property after the record is created. Attempting to set new properties will cause an error. 71 | One benefit of `Record` is that you can now, along with other immutable read methods (.get, .set, .merge and so on), use the dot notation to access properties. 72 | 73 | The creation of a record is less simple than simply calling `.toJS()`. 74 | First, you have to define the `Record` shape. With the example above, to create your initial state, you'll write: 75 | 76 | ```JS 77 | // Defining the shape 78 | const StateRecord = Record({ 79 | myData: { 80 | message: 'Hello World!' 81 | } 82 | }); 83 | 84 | const initialState = new StateRecord({}); // initialState is now a new StateRecord instance 85 | // initialized with myData.message set by default as 'Hello World!' 86 | ``` 87 | 88 | Now, if you want to access `myData`, you can just write `state.myData` in your reducer code and to access the `message` property you can write `state.myData.message` as you would in a plain javascript object. 89 | 90 | ### Gotchas of Using Records 91 | 92 | Although dot notation can now be used to read properties the same does not apply to setting properties. Any attempts to set a property on a `Record` using dot notation will result in errors. 93 | Instead setter methods ( `.set`, `.update`, `.merge`) should be used. 94 | 95 | Certain properties can not be set on a record as they would conflict with the API. Consider the below example: 96 | ```JS 97 | const ProductRecord = Record({ 98 | type: 'tshirt', 99 | size: 'small' 100 | }); 101 | ``` 102 | 103 | Because record.size is used to return the records count (similar to array.length), the above definition would throw an error. -------------------------------------------------------------------------------- /internals/scripts/extract-intl.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * This script will extract the internationalization messages from all components 4 | and package them in the translation json files in the translations file. 5 | */ 6 | const fs = require('fs'); 7 | const nodeGlob = require('glob'); 8 | const transform = require('babel-core').transform; 9 | 10 | const animateProgress = require('./helpers/progress'); 11 | const addCheckmark = require('./helpers/checkmark'); 12 | 13 | const pkg = require('../../package.json'); 14 | const i18n = require('../../app/i18n'); 15 | import { DEFAULT_LOCALE } from '../../app/containers/App/constants'; 16 | 17 | require('shelljs/global'); 18 | 19 | // Glob to match all js files except test files 20 | const FILES_TO_PARSE = 'app/**/!(*.test).js'; 21 | const locales = i18n.appLocales; 22 | 23 | const newLine = () => process.stdout.write('\n'); 24 | 25 | // Progress Logger 26 | let progress; 27 | const task = (message) => { 28 | progress = animateProgress(message); 29 | process.stdout.write(message); 30 | 31 | return (error) => { 32 | if (error) { 33 | process.stderr.write(error); 34 | } 35 | clearTimeout(progress); 36 | return addCheckmark(() => newLine()); 37 | } 38 | } 39 | 40 | // Wrap async functions below into a promise 41 | const glob = (pattern) => new Promise((resolve, reject) => { 42 | nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value))); 43 | }); 44 | 45 | const readFile = (fileName) => new Promise((resolve, reject) => { 46 | fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value))); 47 | }); 48 | 49 | const writeFile = (fileName, data) => new Promise((resolve, reject) => { 50 | fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value))); 51 | }); 52 | 53 | // Store existing translations into memory 54 | const oldLocaleMappings = []; 55 | const localeMappings = []; 56 | // Loop to run once per locale 57 | for (const locale of locales) { 58 | oldLocaleMappings[locale] = {}; 59 | localeMappings[locale] = {}; 60 | // File to store translation messages into 61 | const translationFileName = `app/translations/${locale}.json`; 62 | try { 63 | // Parse the old translation message JSON files 64 | const messages = JSON.parse(fs.readFileSync(translationFileName)); 65 | const messageKeys = Object.keys(messages); 66 | for (const messageKey of messageKeys) { 67 | oldLocaleMappings[locale][messageKey] = messages[messageKey]; 68 | } 69 | } catch (error) { 70 | if (error.code !== 'ENOENT') { 71 | process.stderr.write( 72 | `There was an error loading this translation file: ${translationFileName} 73 | \n${error}` 74 | ); 75 | } 76 | } 77 | } 78 | 79 | const extractFromFile = async (fileName) => { 80 | try { 81 | const code = await readFile(fileName); 82 | // Use babel plugin to extract instances where react-intl is used 83 | const { metadata: result } = await transform(code, { 84 | presets: pkg.babel.presets, 85 | plugins: [ 86 | ['react-intl'], 87 | ], 88 | }); 89 | for (const message of result['react-intl'].messages) { 90 | for (const locale of locales) { 91 | const oldLocaleMapping = oldLocaleMappings[locale][message.id]; 92 | // Merge old translations into the babel extracted instances where react-intl is used 93 | const newMsg = ( locale === DEFAULT_LOCALE) ? message.defaultMessage : ''; 94 | localeMappings[locale][message.id] = (oldLocaleMapping) 95 | ? oldLocaleMapping 96 | : newMsg; 97 | } 98 | } 99 | } catch (error) { 100 | process.stderr.write(`Error transforming file: ${fileName}\n${error}`); 101 | } 102 | }; 103 | 104 | (async function main() { 105 | const memoryTaskDone = task('Storing language files in memory'); 106 | const files = await glob(FILES_TO_PARSE); 107 | memoryTaskDone() 108 | 109 | const extractTaskDone = task('Run extraction on all files'); 110 | // Run extraction on all files that match the glob on line 16 111 | await Promise.all(files.map((fileName) => extractFromFile(fileName))); 112 | extractTaskDone() 113 | 114 | // Make the directory if it doesn't exist, especially for first run 115 | mkdir('-p', 'app/translations'); 116 | for (const locale of locales) { 117 | const translationFileName = `app/translations/${locale}.json`; 118 | 119 | try { 120 | const localeTaskDone = task( 121 | `Writing translation messages for ${locale} to: ${translationFileName}` 122 | ); 123 | 124 | // Sort the translation JSON file so that git diffing is easier 125 | // Otherwise the translation messages will jump around every time we extract 126 | let messages = {}; 127 | Object.keys(localeMappings[locale]).sort().forEach(function(key) { 128 | messages[key] = localeMappings[locale][key]; 129 | }); 130 | 131 | // Write to file the JSON representation of the translation messages 132 | const prettified = `${JSON.stringify(messages, null, 2)}\n`; 133 | 134 | await writeFile(translationFileName, prettified); 135 | 136 | localeTaskDone(); 137 | } catch (error) { 138 | localeTaskDone( 139 | `There was an error saving this translation file: ${translationFileName} 140 | \n${error}` 141 | ); 142 | } 143 | } 144 | 145 | process.exit() 146 | }()); 147 | -------------------------------------------------------------------------------- /docs/general/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The JavaScript ecosystem evolves at incredible speed: staying current can feel 4 | overwhelming. So, instead of you having to stay on top of every new tool, 5 | feature and technique to hit the headlines, this project aims to lighten the 6 | load by providing a curated baseline of the most valuable ones. 7 | 8 | Using React Boilerplate, you get to start your app with our community's current 9 | ideas on what represents optimal developer experience, best practice, most 10 | efficient tooling and cleanest project structure. 11 | 12 | - [**CLI Commands**](commands.md) 13 | - [Tool Configuration](files.md) 14 | - [Server Configurations](server-configs.md) 15 | - [Deployment](deployment.md) *(currently Heroku specific)* 16 | - [FAQ](faq.md) 17 | - [Gotchas](gotchas.md) 18 | 19 | # Feature overview 20 | 21 | ## Quick scaffolding 22 | 23 | Automate the creation of components, containers, routes, selectors and sagas - 24 | and their tests - right from the CLI! 25 | 26 | Run `npm run generate` in your terminal and choose one of the parts you want 27 | to generate. They'll automatically be imported in the correct places and have 28 | everything set up correctly. 29 | 30 | > We use [plop] to generate new components, you can find all the logic and 31 | templates for the generation in `internals/generators`. 32 | 33 | [plop]: https://github.com/amwmedia/plop 34 | 35 | ## Instant feedback 36 | 37 | Enjoy the best DX and code your app at the speed of thought! Your saved changes 38 | to the CSS and JS are reflected instantaneously without refreshing the page. 39 | Preserve application state even when you update something in the underlying code! 40 | 41 | ## Predictable state management 42 | 43 | We use Redux to manage our applications state. We have also added optional 44 | support for the [Chrome Redux DevTools Extension] – if you have it installed, 45 | you can see, play back and change your action history! 46 | 47 | [Chrome Redux DevTools Extension]: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd 48 | 49 | ## Next generation JavaScript 50 | 51 | Use ESNext template strings, object destructuring, arrow functions, JSX syntax 52 | and more, today. This is possible thanks to Babel with the `latest`, `stage-0` 53 | and `react` presets! 54 | 55 | ## Next generation CSS 56 | 57 | Write composable CSS that's co-located with your components using [`styled-components`] 58 | for complete modularity. Unique generated class names keep the specificity low 59 | while eliminating style clashes. Ship only the styles that are used on the 60 | visible page for the best performance. 61 | 62 | [`styled-components`]: ../css/styled-components.md 63 | 64 | ## Industry-standard routing 65 | 66 | It's natural to want to add pages (e.g. `/about`) to your application, and 67 | routing makes this possible. Thanks to [react-router] with [react-router-redux], 68 | that's as easy as pie and the url is auto-synced to your application state! 69 | 70 | [react-router]: https://github.com/reactjs/react-router 71 | [react-router-redux]: https://github.com/reactjs/react-router-redux 72 | 73 | # Optional extras 74 | 75 | _Don't like any of these features? [Click here](remove.md)_ 76 | 77 | ## Offline-first 78 | 79 | The next frontier in performant web apps: availability without a network 80 | connection from the instant your users load the app. This is done with a 81 | ServiceWorker and a fallback to AppCache, so this feature even works on older 82 | browsers! 83 | 84 | > All your files are included automatically. No manual intervention needed 85 | thanks to Webpack's [`offline-plugin`](https://github.com/NekR/offline-plugin) 86 | 87 | ### Add To Homescreen 88 | 89 | After repeat visits to your site, users will get a prompt to add your application 90 | to their homescreen. Combined with offline caching, this means your web app can 91 | be used exactly like a native application (without the limitations of an app store). 92 | 93 | The name and icon to be displayed are set in the `app/manifest.json` file. 94 | Change them to your project name and icon, and try it! 95 | 96 | ## Performant Web Font Loading 97 | 98 | If you simply use web fonts in your project, the page will stay blank until 99 | these fonts are downloaded. That means a lot of waiting time in which users 100 | could already read the content. 101 | 102 | [FontFaceObserver](https://github.com/bramstein/fontfaceobserver) adds a class 103 | to the `body` when the fonts have loaded. (see [`app.js`](../../app/app.js#L26-L36) 104 | and [`App/styles.css`](../../app/containers/App/styles.css)) 105 | 106 | ### Adding a new font 107 | 108 | 1. Either add the `@font-face` declaration to `App/styles.css` or add a `` 109 | tag to the [`index.html`](../../app/index.html). (Don't forget to remove the `` 110 | for Open Sans from the [`index.html`](../../app/index.html)!) 111 | 112 | 2. In `App/styles.css`, specify your initial `font-family` in the `body` tag 113 | with only web-save fonts. In the `body.jsFontLoaded` tag, specify your 114 | `font-family` stack with your web font. 115 | 116 | 3. In `app.js` add a `Observer` for your font. 117 | 118 | ## Image optimization 119 | 120 | Images often represent the majority of bytes downloaded on a web page, so image 121 | optimization can often be a notable performance improvement. Thanks to Webpack's 122 | [`image-loader`](https://github.com/tcoopman/image-webpack-loader), every PNG, JPEG, GIF and SVG images 123 | is optimized. 124 | 125 | See [`image-loader`](https://github.com/tcoopman/image-webpack-loader) to customize optimizations options. 126 | -------------------------------------------------------------------------------- /docs/testing/component-testing.md: -------------------------------------------------------------------------------- 1 | # Component testing 2 | 3 | [Unit testing your Redux actions and reducers](unit-testing.md) is nice, but you 4 | can do even more to make sure nothing breaks your application. Since React is 5 | the _view_ layer of your app, let's see how to test Components too! 6 | 7 | 8 | 9 | - [Shallow rendering](#shallow-rendering) 10 | - [Enzyme](#enzyme) 11 | 12 | 13 | 14 | ## Shallow rendering 15 | 16 | React provides us with a nice add-on called the Shallow Renderer. This renderer 17 | will render a React component **one level deep**. Lets take a look at what that 18 | means with a simple ` 34 | ); 35 | } 36 | 37 | export default Button; 38 | ``` 39 | 40 | _Note: This is a [state**less** ("dumb") component](../js/README.md#architecture-components-and-containers)_ 41 | 42 | It might be used in another component like this: 43 | 44 | ```javascript 45 | // HomePage.react.js 46 | 47 | import Button from './Button.react'; 48 | 49 | class HomePage extends React.Component { 50 | render() { 51 | return( 52 | 53 | ); 54 | } 55 | } 56 | ``` 57 | 58 | _Note: This is a [state**ful** ("smart") component](../js/README.md#architecture-components-and-containers)!_ 59 | 60 | When rendered normally with the standard `ReactDOM.render` function, this will 61 | be the HTML output 62 | (*Comments added in parallel to compare structures in HTML from JSX source*): 63 | 64 | ```html 65 | 69 | ``` 70 | 71 | Conversely, when rendered with the shallow renderer, we'll get a String 72 | containing this "HTML": 73 | 74 | ```html 75 | 79 | ``` 80 | 81 | If we test our `Button` with the normal renderer and there's a problem 82 | with the `CheckmarkIcon` then the test for the `Button` will fail as well... 83 | but finding the culprit will be hard. Using the _shallow_ renderer, we isolate 84 | the problem's cause since we don't render any other components other than the 85 | one we're testing! 86 | 87 | The problem with the shallow renderer is that all assertions have to be done 88 | manually, and you cannot do anything that needs the DOM. 89 | 90 | Thankfully, [AirBnB](https://twitter.com/AirbnbEng) has open sourced their 91 | wrapper around the React shallow renderer and jsdom, called `enzyme`. `enzyme` 92 | is a testing utility that gives us a nice assertion/traversal/manipulation API. 93 | 94 | ## Enzyme 95 | 96 | Lets test our ` 119 | ); 120 | expect( 121 | renderedComponent.find("button").node 122 | ).toExist(); 123 | }); 124 | ``` 125 | 126 | Nice! If somebody breaks our button component by having it render an `` tag 127 | or something else we'll immediately know! Let's do something a bit more advanced 128 | now, and check that our ` 138 | ); 139 | expect( 140 | renderedComponent.contains(text) 141 | ).toEqual(true); 142 | }); 143 | ``` 144 | 145 | Great! Onwards to our last and most advanced test: checking that our `