├── internals ├── templates │ ├── translations │ │ └── en.json │ ├── styles.css │ ├── languageProvider │ │ ├── constants.js │ │ ├── actions.js │ │ ├── selectors.js │ │ ├── reducer.js │ │ └── languageProvider.js │ ├── homePage │ │ ├── messages.js │ │ └── homePage.js │ ├── notFoundPage │ │ ├── messages.js │ │ └── notFoundPage.js │ ├── selectors.test.js │ ├── selectors.js │ ├── store.test.js │ ├── i18n.js │ ├── index.html │ ├── appContainer.js │ ├── reducers.js │ ├── routes.js │ ├── store.js │ ├── asyncInjectors.js │ └── app.js ├── generators │ ├── language │ │ ├── translations-json.hbs │ │ ├── app-locale.hbs │ │ ├── add-locale-data.hbs │ │ ├── polyfill-intl-locale.hbs │ │ ├── intl-locale-data.hbs │ │ ├── format-translation-messages.hbs │ │ ├── translation-messages.hbs │ │ └── index.js │ ├── component │ │ ├── styles.css.hbs │ │ ├── test.js.hbs │ │ ├── messages.js.hbs │ │ ├── stateless.js.hbs │ │ ├── es6.js.hbs │ │ └── index.js │ ├── container │ │ ├── styles.css.hbs │ │ ├── constants.js.hbs │ │ ├── actions.js.hbs │ │ ├── sagas.js.hbs │ │ ├── test.js.hbs │ │ ├── reducer.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 │ ├── route │ │ ├── route.hbs │ │ ├── routeWithReducer.hbs │ │ └── index.js │ ├── utils │ │ └── componentExists.js │ └── index.js ├── scripts │ ├── helpers │ │ ├── checkmark.js │ │ └── progress.js │ ├── npmcheckversion.js │ ├── pagespeed.js │ ├── analyze.js │ ├── dependencies.js │ ├── setup.js │ └── clean.js ├── testing │ ├── test-bundler.js │ └── karma.conf.js ├── webpack │ ├── webpack.dll.babel.js │ ├── webpack.test.babel.js │ └── webpack.base.babel.js └── config.js ├── app ├── components │ ├── H2 │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Toggle │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── H1 │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── A │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Footer │ │ ├── styles.css │ │ ├── messages.js │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── H3 │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── ListItem │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── IssueIcon │ │ ├── tests │ │ │ └── index.test.js │ │ └── index.js │ ├── List │ │ ├── styles.css │ │ ├── tests │ │ │ └── index.test.js │ │ └── index.js │ ├── LoadingIndicator │ │ ├── tests │ │ │ └── index.test.js │ │ ├── index.js │ │ └── styles.css │ ├── ToggleOption │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Img │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ └── Button │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ └── index.test.js ├── containers │ ├── LocaleToggle │ │ ├── styles.css │ │ ├── messages.js │ │ ├── tests │ │ │ ├── messages.test.js │ │ │ └── index.test.js │ │ └── index.js │ ├── App │ │ ├── banner-metal.jpg │ │ ├── constants.js │ │ ├── styles.css │ │ ├── tests │ │ │ ├── index.test.js │ │ │ ├── actions.test.js │ │ │ ├── reducer.test.js │ │ │ └── selectors.test.js │ │ ├── selectors.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── actions.js │ ├── LanguageProvider │ │ ├── constants.js │ │ ├── actions.js │ │ ├── tests │ │ │ ├── reducer.test.js │ │ │ ├── selectors.test.js │ │ │ ├── actions.test.js │ │ │ └── index.test.js │ │ ├── selectors.js │ │ ├── reducer.js │ │ └── index.js │ ├── FeaturePage │ │ ├── styles.css │ │ ├── tests │ │ │ └── index.test.js │ │ └── index.js │ ├── HomePage │ │ ├── selectors.js │ │ ├── constants.js │ │ ├── styles.css │ │ ├── tests │ │ │ ├── actions.test.js │ │ │ ├── reducer.test.js │ │ │ ├── selectors.test.js │ │ │ ├── index.test.js │ │ │ └── sagas.test.js │ │ ├── reducer.js │ │ ├── actions.js │ │ ├── messages.js │ │ └── sagas.js │ ├── NotFoundPage │ │ ├── messages.js │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ └── RepoListItem │ │ ├── styles.css │ │ ├── index.js │ │ └── tests │ │ └── index.test.js ├── favicon.ico ├── tests │ └── store.test.js ├── manifest.json ├── i18n.js ├── index.html ├── utils │ ├── request.js │ ├── tests │ │ └── request.test.js │ └── asyncInjectors.js ├── reducers.js ├── .nginx.conf ├── store.js ├── .htaccess └── routes.js ├── docs ├── general │ ├── webstorm-debug.png │ ├── webstorm-eslint.png │ ├── server-configs.md │ ├── gotchas.md │ ├── deployment.md │ ├── files.md │ └── remove.md ├── css │ ├── stylelint.md │ ├── README.md │ ├── sanitize.md │ ├── remove.md │ ├── sass.md │ └── postcss.md ├── testing │ ├── remote-testing.md │ └── README.md └── js │ ├── remove.md │ ├── README.md │ ├── redux.md │ ├── reselect.md │ ├── redux-saga.md │ └── immutablejs.md ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── LICENSE.md ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── server ├── logger.js ├── index.js └── middlewares │ ├── frontendMiddleware.js │ └── api.js ├── appveyor.yml ├── .gitattributes ├── CODE_OF_CONDUCT.md └── Changelog.md /internals/templates/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | '{{language}}', 3 | -------------------------------------------------------------------------------- /app/components/H2/styles.css: -------------------------------------------------------------------------------- 1 | .heading2 { 2 | font-size: 1.5em; 3 | } 4 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/styles.css: -------------------------------------------------------------------------------- 1 | .localeToggle { 2 | padding: 2px; 3 | } 4 | -------------------------------------------------------------------------------- /app/components/Toggle/styles.css: -------------------------------------------------------------------------------- 1 | .toggle { 2 | line-height: 1em; 3 | height: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikswan/react-boilerplate/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /app/components/H1/styles.css: -------------------------------------------------------------------------------- 1 | .heading1 { 2 | font-size: 2em; 3 | margin-bottom: 0.25em; 4 | } 5 | -------------------------------------------------------------------------------- /internals/generators/component/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /internals/generators/container/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /app/components/A/styles.css: -------------------------------------------------------------------------------- 1 | .link { 2 | color: #41ADDD; 3 | } 4 | 5 | .link:hover { 6 | color: #6CC0E5; 7 | } 8 | -------------------------------------------------------------------------------- /internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /docs/general/webstorm-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikswan/react-boilerplate/HEAD/docs/general/webstorm-debug.png -------------------------------------------------------------------------------- /docs/general/webstorm-eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikswan/react-boilerplate/HEAD/docs/general/webstorm-eslint.png -------------------------------------------------------------------------------- /internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /app/containers/App/banner-metal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikswan/react-boilerplate/HEAD/app/containers/App/banner-metal.jpg -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages({{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /internals/templates/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * styles.css 3 | * 4 | * App container styles 5 | */ 6 | 7 | .container { 8 | display: block; 9 | } 10 | -------------------------------------------------------------------------------- /app/components/Footer/styles.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 3em 0; 5 | border-top: 1px solid #666; 6 | } 7 | -------------------------------------------------------------------------------- /.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/H3/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function H3(props) { 4 | return ( 5 |

6 | ); 7 | } 8 | 9 | export default H3; 10 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /internals/templates/languageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/components/H1/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | function H1(props) { 6 | return ( 7 |

8 | ); 9 | } 10 | 11 | export default H1; 12 | -------------------------------------------------------------------------------- /app/components/H2/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | function H2(props) { 6 | return ( 7 |

8 | ); 9 | } 10 | 11 | export default H2; 12 | -------------------------------------------------------------------------------- /app/containers/FeaturePage/styles.css: -------------------------------------------------------------------------------- 1 | .list { 2 | font-family: Georgia, Times, 'Times New Roman', serif; 3 | padding-left: 1.75em; 4 | } 5 | 6 | .listItem { 7 | margin: 1em 0; 8 | } 9 | 10 | .listItemTitle { 11 | font-weight: bold; 12 | } 13 | -------------------------------------------------------------------------------- /internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 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/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/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 | -------------------------------------------------------------------------------- /internals/templates/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/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var exec = require('child_process').exec; 3 | exec('npm -v', function (err, stdout, stderr) { 4 | if (err) throw err; 5 | if (parseFloat(stdout) < 3) { 6 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 7 | process.exit(1); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /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/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/templates/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.components.HomePage.header', 11 | defaultMessage: 'This is HomePage components !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/ListItem/styles.css: -------------------------------------------------------------------------------- 1 | .item { 2 | width: 100%; 3 | height: 3em; 4 | display: flex; 5 | align-items: center; 6 | position: relative; 7 | } 8 | 9 | .item + .item { 10 | border-top: 1px solid #EEE; 11 | } 12 | 13 | .itemContent { 14 | display: flex; 15 | justify-content: space-between; 16 | width: 100%; 17 | height: 100%; 18 | align-items: center; 19 | } 20 | -------------------------------------------------------------------------------- /app/containers/HomePage/selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Homepage selectors 3 | */ 4 | 5 | import { createSelector } from 'reselect'; 6 | 7 | const selectHome = () => (state) => state.get('home'); 8 | 9 | const selectUsername = () => createSelector( 10 | selectHome(), 11 | (homeState) => homeState.get('username') 12 | ); 13 | 14 | export { 15 | selectHome, 16 | selectUsername, 17 | }; 18 | -------------------------------------------------------------------------------- /internals/templates/notFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import languageProviderReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('languageProviderReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(languageProviderReducer(undefined, {})).toEqual(fromJS({ 8 | locale: 'en', 9 | })); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /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/components/IssueIcon/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import IssueIcon 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 SVG', () => { 9 | const renderedComponent = shallow( 10 | 11 | ); 12 | expect(renderedComponent.find('svg').length).toEqual(1); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/css/stylelint.md: -------------------------------------------------------------------------------- 1 | # stylelint 2 | 3 | stylelint catches bugs and helps keep you and your team on consistent with the 4 | standards and conventions you define. 5 | 6 | We've pre-configured it to extend [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) 7 | but you can (and should!) adapt it to your house style. 8 | 9 | See the [official documentation](http://stylelint.io/) for more information! 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 its text', () => { 9 | const children = 'Text'; 10 | const renderedComponent = shallow( 11 |

{children}

12 | ); 13 | expect(renderedComponent.contains(children)).toEqual(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/components/H2/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import H2 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 its text', () => { 9 | const children = 'Text'; 10 | const renderedComponent = shallow( 11 |

{children}

12 | ); 13 | expect(renderedComponent.contains(children)).toEqual(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/components/H3/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import H3 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 its text', () => { 9 | const children = 'Text'; 10 | const renderedComponent = shallow( 11 |

{children}

12 | ); 13 | expect(renderedComponent.contains(children)).toEqual(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/components/List/styles.css: -------------------------------------------------------------------------------- 1 | /* Gives the scrollbar rounded edges via overflow: hidden */ 2 | .listWrapper { 3 | padding: 0; 4 | margin: 0; 5 | width: 100%; 6 | background-color: white; 7 | border: 1px solid #CCC; 8 | border-radius: 3px; 9 | overflow: hidden; 10 | } 11 | 12 | .list { 13 | list-style: none; 14 | padding: 0; 15 | margin: 0; 16 | width: 100%; 17 | max-height: 30em; 18 | overflow-y: auto; 19 | padding: 0 1em; 20 | } 21 | -------------------------------------------------------------------------------- /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/components/LoadingIndicator/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import LoadingIndicator 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 14 divs', () => { 9 | const renderedComponent = shallow( 10 | 11 | ); 12 | expect(renderedComponent.find('div').length).toEqual(14); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/css/README.md: -------------------------------------------------------------------------------- 1 | # CSS 2 | 3 | This boilerplate uses PostCSS as a CSS preprocessor with a few utility plugins 4 | to make it "batteries included". 5 | 6 | CSS Modules lets us embrace component encapsulation while sanitize.css gives us 7 | data-driven cross-browser normalisation. 8 | 9 | Learn more: 10 | 11 | - [PostCSS](postcss.md) 12 | - [CSS Modules](css-modules.md) 13 | - [sanitize.css](sanitize.md) 14 | - [stylelint.css](stylelint.md) 15 | - [Using Sass](sass.md) 16 | -------------------------------------------------------------------------------- /app/components/ListItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | function ListItem(props) { 6 | return ( 7 |
  • 8 |
    9 | {props.item} 10 |
    11 |
  • 12 | ); 13 | } 14 | 15 | ListItem.propTypes = { 16 | className: React.PropTypes.string, 17 | item: React.PropTypes.any, 18 | }; 19 | 20 | export default ListItem; 21 | -------------------------------------------------------------------------------- /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 | 12 | const selectLocale = () => createSelector( 13 | selectLanguage(), 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | selectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'boilerplate.containers.NotFoundPage.header', 11 | defaultMessage: 'Page not found.', 12 | }, 13 | homeButton: { 14 | id: 'boilerplate.containers.NotFoundPage.home', 15 | defaultMessage: 'Home', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /internals/templates/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 | 12 | const selectLocale = () => createSelector( 13 | selectLanguage(), 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | selectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/containers/RepoListItem/styles.css: -------------------------------------------------------------------------------- 1 | .linkWrapper { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | align-items: space-between; 6 | } 7 | 8 | .linkRepo { 9 | height: 100%; 10 | color: black; 11 | display: flex; 12 | align-items: center; 13 | width: 100%; 14 | } 15 | 16 | .linkIssues { 17 | color: black; 18 | height: 100%; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .issueIcon { 25 | fill: #CCC; 26 | margin-right: 0.25em; 27 | } 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | selectLanguage, 3 | } from '../selectors'; 4 | import { fromJS } from 'immutable'; 5 | import expect from 'expect'; 6 | 7 | describe('selectLanguage', () => { 8 | const globalSelector = selectLanguage(); 9 | it('should select the global state', () => { 10 | const globalState = fromJS({}); 11 | const mockedState = fromJS({ 12 | language: globalState, 13 | }); 14 | expect(globalSelector(mockedState)).toEqual(globalState); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internals/templates/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/containers/LanguageProvider/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | changeLocale, 4 | } from '../actions'; 5 | import { 6 | CHANGE_LOCALE, 7 | } from '../constants'; 8 | 9 | describe('LanguageProvider actions', () => { 10 | describe('Change Local Action', () => { 11 | it('has a type of CHANGE_LOCALE', () => { 12 | const expected = { 13 | type: CHANGE_LOCALE, 14 | locale: 'de', 15 | }; 16 | expect(changeLocale('de')).toEqual(expected); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/components/ToggleOption/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ToggleOption 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { injectIntl, intlShape } from 'react-intl'; 9 | 10 | const ToggleOption = ({ value, message, intl }) => ( 11 | 14 | ); 15 | 16 | ToggleOption.propTypes = { 17 | value: React.PropTypes.string.isRequired, 18 | message: React.PropTypes.object.isRequired, 19 | intl: intlShape.isRequired, 20 | }; 21 | 22 | export default injectIntl(ToggleOption); 23 | -------------------------------------------------------------------------------- /app/components/IssueIcon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function IssueIcon(props) { 4 | return ( 5 | 10 | 11 | 12 | ); 13 | } 14 | 15 | IssueIcon.propTypes = { 16 | className: React.PropTypes.string, 17 | }; 18 | 19 | export default IssueIcon; 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | dist: trusty 4 | node_js: 5 | - "5.0" 6 | script: npm run build 7 | before_install: 8 | - export CHROME_BIN=/usr/bin/google-chrome 9 | - export DISPLAY=:99.0 10 | - sudo apt-get update 11 | - sudo apt-get install -y libappindicator1 fonts-liberation 12 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 13 | - sudo dpkg -i google-chrome*.deb 14 | - sh -e /etc/init.d/xvfb start 15 | notifications: 16 | email: 17 | on_failure: change 18 | after_success: 'npm run coveralls' 19 | -------------------------------------------------------------------------------- /app/components/Footer/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Footer Messages 3 | * 4 | * This contains all the text for the Footer component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | licenseMessage: { 10 | id: 'boilerplate.components.Footer.license.message', 11 | defaultMessage: 'This project is licensed under the MIT license.', 12 | }, 13 | authorMessage: { 14 | id: 'boilerplate.components.Footer.author.message', 15 | defaultMessage: ` 16 | Made with love by {author}. 17 | `, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /internals/templates/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 | -------------------------------------------------------------------------------- /app/components/A/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A link to a certain page, an anchor tag 3 | */ 4 | 5 | import React, { PropTypes } from 'react'; 6 | 7 | import styles from './styles.css'; 8 | 9 | function A(props) { 10 | return ( 11 | 17 | ); 18 | } 19 | 20 | A.propTypes = { 21 | className: PropTypes.string, 22 | href: PropTypes.string.isRequired, 23 | target: PropTypes.string, 24 | children: PropTypes.node.isRequired, 25 | }; 26 | 27 | export default A; 28 | -------------------------------------------------------------------------------- /app/containers/HomePage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomeConstants 3 | * Each action has a corresponding type, which the reducer knows and picks up on. 4 | * To avoid weird typos between the reducer and the actions, we save them as 5 | * constants here. We prefix them with 'yourproject/YourComponent' so we avoid 6 | * reducers accidentally picking up actions they shouldn't. 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | export const CHANGE_USERNAME = 'boilerplate/Home/CHANGE_USERNAME'; 13 | -------------------------------------------------------------------------------- /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 | 12 | const initialState = fromJS({ 13 | locale: 'en', 14 | }); 15 | 16 | function languageProviderReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | return state 20 | .set('locale', action.locale); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default languageProviderReducer; 27 | -------------------------------------------------------------------------------- /app/components/Img/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Img.react.js 4 | * 5 | * Renders an image, enforcing the usage of the alt="" tag 6 | */ 7 | 8 | import React, { PropTypes } from 'react'; 9 | 10 | function Img(props) { 11 | return ( 12 | {props.alt} 13 | ); 14 | } 15 | 16 | // We require the use of src and alt, only enforced by react in dev mode 17 | Img.propTypes = { 18 | src: PropTypes.string.isRequired, 19 | alt: PropTypes.string.isRequired, 20 | className: PropTypes.string, 21 | }; 22 | 23 | export default Img; 24 | -------------------------------------------------------------------------------- /internals/templates/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 | 12 | const initialState = fromJS({ 13 | locale: 'en', 14 | }); 15 | 16 | function languageProviderReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | return state 20 | .set('locale', action.locale); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default languageProviderReducer; 27 | -------------------------------------------------------------------------------- /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 isparta code coverage 12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes)).)*\.js$/); 13 | context.keys().forEach(context); 14 | -------------------------------------------------------------------------------- /app/containers/HomePage/styles.css: -------------------------------------------------------------------------------- 1 | .textSection { 2 | margin: 3em auto; 3 | } 4 | 5 | .textSection:first-child { 6 | margin-top: 0; 7 | } 8 | 9 | .centered { 10 | text-align: center; 11 | } 12 | 13 | p, 14 | label { 15 | font-family: Georgia, Times, 'Times New Roman', serif; 16 | line-height: 1.5em; 17 | } 18 | 19 | .link { 20 | text-decoration: none; 21 | } 22 | 23 | .usernameForm { 24 | margin-bottom: 1em; 25 | } 26 | 27 | .input { 28 | outline: none; 29 | border-bottom: 1px dotted #999; 30 | } 31 | 32 | .atPrefix { 33 | color: black; 34 | margin-left: 0.4em; 35 | } 36 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { 4 | CHANGE_USERNAME, 5 | } from '../constants'; 6 | 7 | import { 8 | changeUsername, 9 | } from '../actions'; 10 | 11 | describe('Home Actions', () => { 12 | describe('changeUsername', () => { 13 | it('should return the correct type and the passed name', () => { 14 | const fixture = 'Max'; 15 | const expectedResult = { 16 | type: CHANGE_USERNAME, 17 | name: fixture, 18 | }; 19 | 20 | expect(changeUsername(fixture)).toEqual(expectedResult); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /app/components/Img/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import Img from '../index'; 2 | 3 | import { expect } from 'chai'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('', () => { 8 | it('should render an tag', () => { 9 | const renderedComponent = shallow(test); 10 | expect(renderedComponent).to.have.tagName('img'); 11 | }); 12 | 13 | it('should have an alt attribute', () => { 14 | const renderedComponent = shallow(test); 15 | expect(renderedComponent).to.have.attr('alt', 'test'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LocaleToggle Messages 3 | * 4 | * This contains all the text for the LanguageToggle component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | import { appLocales } from '../../i18n'; 8 | 9 | export function getLocaleMessages(locales) { 10 | return locales.reduce((messages, locale) => 11 | Object.assign(messages, { 12 | [locale]: { 13 | id: `app.components.LocaleToggle.${locale}`, 14 | defaultMessage: `${locale}`, 15 | }, 16 | }), {}); 17 | } 18 | 19 | export default defineMessages( 20 | getLocaleMessages(appLocales) 21 | ); 22 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/tests/messages.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { getLocaleMessages } from '../messages'; 3 | 4 | describe('getLocaleMessages', () => { 5 | it('should create i18n messages for all locales', () => { 6 | const expected = { 7 | en: { 8 | id: 'app.components.LocaleToggle.en', 9 | defaultMessage: 'en', 10 | }, 11 | fr: { 12 | id: 'app.components.LocaleToggle.fr', 13 | defaultMessage: 'fr', 14 | }, 15 | }; 16 | 17 | const actual = getLocaleMessages(['en', 'fr']); 18 | 19 | assert.deepEqual(expected, actual); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This is a fork from https://github.com/mxstbr/react-boilerplate. 2 | 3 | It's only purpose is to make it easier for people to follow along with my "Scaling React Apps" course on pluralsight.com: https://app.pluralsight.com/library/courses/react-boilerplate-building-scalable-apps. 4 | 5 | By working from this fork, you are guaranteed that you will get exactly the same versions that I used when I built the course. I also made 1 tiny change in the boilerplate to make the course easier to follow along. This is mentioned in the course, but it is important enough to mention here too. 6 | 7 | This is not my boilerplate! **It's just a fork with a few tweaks for the course.** 8 | -------------------------------------------------------------------------------- /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/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | * Each action has a corresponding type, which the reducer knows and picks up on. 4 | * To avoid weird typos between the reducer and the actions, we save them as 5 | * constants here. We prefix them with 'yourproject/YourComponent' so we avoid 6 | * reducers accidentally picking up actions they shouldn't. 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | export const LOAD_REPOS = 'boilerplate/App/LOAD_REPOS'; 13 | export const LOAD_REPOS_SUCCESS = 'boilerplate/App/LOAD_REPOS_SUCCESS'; 14 | export const LOAD_REPOS_ERROR = 'boilerplate/App/LOAD_REPOS_ERROR'; 15 | -------------------------------------------------------------------------------- /app/components/ListItem/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import ListItem from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('', () => { 8 | it('should adopt the className', () => { 9 | const renderedComponent = shallow(); 10 | expect(renderedComponent.find('li').hasClass('test')).toEqual(true); 11 | }); 12 | 13 | it('should render the content passed to it', () => { 14 | const content = 'Hello world!'; 15 | const renderedComponent = shallow( 16 | 17 | ); 18 | expect(renderedComponent.contains(content)).toEqual(true); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | 14 | {{#if wantCSS}} 15 | import styles from './styles.css'; 16 | {{/if}} 17 | 18 | function {{ properCase name }}() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | 32 | export default {{ properCase name }}; 33 | -------------------------------------------------------------------------------- /app/components/Button/styles.css: -------------------------------------------------------------------------------- 1 | .buttonWrapper { 2 | width: 100%; 3 | text-align: center; 4 | margin: 4em 0; 5 | } 6 | 7 | .button { 8 | display: inline-block; 9 | box-sizing: border-box; 10 | padding: 0.25em 2em; 11 | margin: 0; 12 | border: 0; 13 | text-decoration: none; 14 | border-radius: 4px; 15 | -webkit-font-smoothing: antialiased; 16 | -webkit-touch-callout: none; 17 | user-select: none; 18 | cursor: pointer; 19 | outline: 0; 20 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 21 | font-weight: bold; 22 | font-size: 16px; 23 | color: #FFF; 24 | border: 2px solid #41ADDD; 25 | color: #41ADDD; 26 | } 27 | 28 | .button:active { 29 | background: #41ADDD; 30 | color: #FFF; 31 | } 32 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; 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 | var readline = require('readline'); 2 | 3 | /** 4 | * Adds an animated progress indicator 5 | * 6 | * @param {string} message The message to write next to the indicator 7 | * @param {number} amountOfDots The amount of dots you want to animate 8 | */ 9 | function animateProgress(message, amountOfDots) { 10 | if (typeof amountOfDots !== 'number') { 11 | amountOfDots = 3; 12 | } 13 | 14 | var i = 0; 15 | return setInterval(function () { 16 | readline.cursorTo(process.stdout, 0); 17 | i = (i + 1) % (amountOfDots + 1); 18 | var dots = new Array(i + 1).join('.'); 19 | process.stdout.write(message + dots); 20 | }, 500); 21 | } 22 | 23 | module.exports = animateProgress; 24 | -------------------------------------------------------------------------------- /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/containers/App/styles.css: -------------------------------------------------------------------------------- 1 | /* Global styles */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | body { 10 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 11 | } 12 | 13 | body.fontLoaded { 14 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 15 | } 16 | 17 | :global(#app) { 18 | background-color: #FAFAFA; 19 | min-height: 100%; 20 | min-width: 100%; 21 | } 22 | 23 | .wrapper { 24 | max-width: calc(768px + 16px * 2); 25 | margin: 0 auto; 26 | display: flex; 27 | min-height: 100%; 28 | padding: 0 16px; 29 | flex-direction: column; 30 | } 31 | 32 | .logoWrapper { 33 | padding: 2em 0; 34 | } 35 | 36 | .logo { 37 | width: 100%; 38 | margin: 0 auto; 39 | display: block; 40 | } 41 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /internals/templates/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 | -------------------------------------------------------------------------------- /app/components/List/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { render } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | import List from '../index'; 6 | import ListItem from 'components/ListItem'; 7 | 8 | describe('', () => { 9 | it('should render the component if no items are passed', () => { 10 | const renderedComponent = render( 11 | 12 | ); 13 | expect(renderedComponent.find(ListItem)).toExist(); 14 | }); 15 | 16 | it('should render the items', () => { 17 | const items = [ 18 | 'Hello', 19 | 'World', 20 | ]; 21 | const renderedComponent = render( 22 | 23 | ); 24 | expect(renderedComponent.find(items)).toExist(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /internals/templates/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 | 9 | import enLocaleData from 'react-intl/locale-data/en'; 10 | 11 | export const appLocales = [ 12 | 'en', 13 | ]; 14 | 15 | import enTranslationMessages from './translations/en.json'; 16 | 17 | addLocaleData(enLocaleData); 18 | 19 | const formatTranslationMessages = (messages) => { 20 | const formattedMessages = {}; 21 | for (const message of messages) { 22 | formattedMessages[message.id] = message.message || message.defaultMessage; 23 | } 24 | 25 | return formattedMessages; 26 | }; 27 | 28 | export const translationMessages = { 29 | en: formatTranslationMessages(enTranslationMessages), 30 | }; 31 | -------------------------------------------------------------------------------- /internals/templates/homePage/homePage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage 3 | * 4 | * This is the first thing users see of our App, at the '/' route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | 18 | render() { 19 | return ( 20 |

    21 | 22 |

    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import homeReducer from '../reducer'; 3 | import { 4 | changeUsername, 5 | } from '../actions'; 6 | import { fromJS } from 'immutable'; 7 | 8 | describe('homeReducer', () => { 9 | let state; 10 | beforeEach(() => { 11 | state = fromJS({ 12 | username: '', 13 | }); 14 | }); 15 | 16 | it('should return the initial state', () => { 17 | const expectedResult = state; 18 | expect(homeReducer(undefined, {})).toEqual(expectedResult); 19 | }); 20 | 21 | it('should handle the changeUsername action correctly', () => { 22 | const fixture = 'mxstbr'; 23 | const expectedResult = state.set('username', fixture); 24 | 25 | expect(homeReducer(state, changeUsername(fixture))).toEqual(expectedResult); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.stdin.resume(); 4 | process.stdin.setEncoding('utf8'); 5 | 6 | var ngrok = require('ngrok'); 7 | var psi = require('psi'); 8 | var 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 | -------------------------------------------------------------------------------- /internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | {{#if wantCSS}} 14 | import styles from './styles.css'; 15 | {{/if}} 16 | 17 | class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default {{ properCase name }}; 34 | -------------------------------------------------------------------------------- /internals/templates/notFoundPage/notFoundPage.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 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | 18 | render() { 19 | return ( 20 |

    21 | 22 |

    23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/components/List/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | function List(props) { 6 | const ComponentToRender = props.component; 7 | let content = (
    ); 8 | 9 | // If we have items, render them 10 | if (props.items) { 11 | content = props.items.map((item, index) => ( 12 | 13 | )); 14 | } else { 15 | // Otherwise render a single component 16 | content = (); 17 | } 18 | 19 | return ( 20 |
    21 |
      22 | {content} 23 |
    24 |
    25 | ); 26 | } 27 | 28 | List.propTypes = { 29 | component: React.PropTypes.func.isRequired, 30 | items: React.PropTypes.array, 31 | }; 32 | 33 | export default List; 34 | -------------------------------------------------------------------------------- /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/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 |
    16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/components/ToggleOption/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import ToggleOption from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import { IntlProvider, defineMessages } from 'react-intl'; 6 | import React from 'react'; 7 | 8 | describe('', () => { 9 | it('should render default language messages', () => { 10 | const defaultEnMessage = 'someContent'; 11 | const message = defineMessages({ 12 | enMessage: { 13 | id: 'app.components.LocaleToggle.en', 14 | defaultMessage: defaultEnMessage, 15 | }, 16 | }); 17 | const renderedComponent = shallow( 18 | 19 | 20 | 21 | ); 22 | expect(renderedComponent.contains()).toEqual(true); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { 5 | selectHome, 6 | selectUsername, 7 | } from '../selectors'; 8 | 9 | describe('selectHome', () => { 10 | const homeSelector = selectHome(); 11 | it('should select the home state', () => { 12 | const homeState = fromJS({ 13 | userData: {}, 14 | }); 15 | const mockedState = fromJS({ 16 | home: homeState, 17 | }); 18 | expect(homeSelector(mockedState)).toEqual(homeState); 19 | }); 20 | }); 21 | 22 | describe('selectUsername', () => { 23 | const usernameSelector = selectUsername(); 24 | it('should select the username', () => { 25 | const username = 'mxstbr'; 26 | const mockedState = fromJS({ 27 | home: { 28 | username, 29 | }, 30 | }); 31 | expect(usernameSelector(mockedState)).toEqual(username); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shelljs = require('shelljs'); 4 | var animateProgress = require('./helpers/progress'); 5 | var chalk = require('chalk'); 6 | var addCheckMark = require('./helpers/checkmark'); 7 | 8 | var 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 | -------------------------------------------------------------------------------- /app/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import messages from './messages'; 4 | import A from 'components/A'; 5 | import styles from './styles.css'; 6 | import { FormattedMessage } from 'react-intl'; 7 | import LocaleToggle from 'containers/LocaleToggle'; 8 | 9 | function Footer() { 10 | return ( 11 |
    31 | ); 32 | } 33 | 34 | export default Footer; 35 | -------------------------------------------------------------------------------- /app/containers/App/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | import App from '../index'; 6 | import Footer from 'components/Footer'; 7 | 8 | describe('', () => { 9 | it('should render the logo', () => { 10 | const renderedComponent = shallow( 11 | 12 | ); 13 | expect(renderedComponent.find('Img').length).toEqual(1); 14 | }); 15 | 16 | it('should render its children', () => { 17 | const children = (

    Test

    ); 18 | const renderedComponent = shallow( 19 | 20 | {children} 21 | 22 | ); 23 | expect(renderedComponent.contains(children)).toEqual(true); 24 | }); 25 | 26 | it('should render the footer', () => { 27 | const renderedComponent = shallow( 28 | 29 | ); 30 | expect(renderedComponent.find(Footer).length).toEqual(1); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LocaleToggle 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | // import { FormattedMessage } from 'react-intl'; 10 | import styles from './styles.css'; 11 | import ToggleOption from '../ToggleOption'; 12 | 13 | function Toggle(props) { // eslint-disable-line react/prefer-stateless-function 14 | let content = (); 15 | 16 | // If we have items, render them 17 | if (props.values) { 18 | content = props.values.map((value) => ( 19 | 20 | )); 21 | } 22 | 23 | return ( 24 | 27 | ); 28 | } 29 | 30 | Toggle.propTypes = { 31 | onToggle: React.PropTypes.func, 32 | values: React.PropTypes.array, 33 | messages: React.PropTypes.object, 34 | }; 35 | 36 | export default Toggle; 37 | -------------------------------------------------------------------------------- /app/containers/HomePage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomeReducer 3 | * 4 | * The reducer takes care of our data. Using actions, we can change our 5 | * application state. 6 | * To add a new action, add it to the switch statement in the reducer function 7 | * 8 | * Example: 9 | * case YOUR_ACTION_CONSTANT: 10 | * return state.set('yourStateVariable', true); 11 | */ 12 | 13 | import { 14 | CHANGE_USERNAME, 15 | } from './constants'; 16 | import { fromJS } from 'immutable'; 17 | 18 | // The initial state of the App 19 | const initialState = fromJS({ 20 | username: '', 21 | }); 22 | 23 | function homeReducer(state = initialState, action) { 24 | switch (action.type) { 25 | case CHANGE_USERNAME: 26 | 27 | // Delete prefixed '@' from the github username 28 | return state 29 | .set('username', action.name.replace(/@/gi, '')); 30 | default: 31 | return state; 32 | } 33 | } 34 | 35 | export default homeReducer; 36 | -------------------------------------------------------------------------------- /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/templates/appContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a neccessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | 16 | import styles from './styles.css'; 17 | 18 | export default class App extends React.Component { // eslint-disable-line react/prefer-stateless-function 19 | 20 | static propTypes = { 21 | children: React.PropTypes.node, 22 | }; 23 | 24 | render() { 25 | return ( 26 |
    27 | {React.Children.toArray(this.props.children)} 28 |
    29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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, delete the `sagas/` folder, remove the 7 | `import` and the `sagaMiddleware` from the `store.js` and finally remove it from 8 | the `package.json`. Then you should be good to go with whatever side-effect 9 | management library you want to use! 10 | 11 | ## Removing `reselect` 12 | 13 | To remove `reselect`, delete the `app/selectors` folder, remove it from your 14 | dependencies in `package.json` and then write your `mapStateToProps` functions 15 | like you normally would! 16 | 17 | You'll also need to hook up the history directly to the store. Change the const 18 | `history` in `app/app.js` to the following: 19 | 20 | ```js 21 | const history = syncHistoryWithStore(browserHistory, store, { 22 | selectLocationState: (state) => state.get('route').toJS(), 23 | }); 24 | ``` 25 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | function LoadingIndicator() { 6 | return ( 7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 | ); 24 | } 25 | 26 | export default LoadingIndicator; 27 | -------------------------------------------------------------------------------- /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 | 9 | import enLocaleData from 'react-intl/locale-data/en'; 10 | import deLocaleData from 'react-intl/locale-data/de'; 11 | 12 | addLocaleData(enLocaleData); 13 | addLocaleData(deLocaleData); 14 | 15 | export const appLocales = [ 16 | 'en', 17 | 'de', 18 | ]; 19 | 20 | import enTranslationMessages from './translations/en.json'; 21 | import deTranslationMessages from './translations/de.json'; 22 | 23 | export const formatTranslationMessages = (messages) => { 24 | const formattedMessages = {}; 25 | for (const message of messages) { 26 | formattedMessages[message.id] = message.message || message.defaultMessage; 27 | } 28 | 29 | return formattedMessages; 30 | }; 31 | 32 | export const translationMessages = { 33 | en: formatTranslationMessages(enTranslationMessages), 34 | de: formatTranslationMessages(deTranslationMessages), 35 | }; 36 | -------------------------------------------------------------------------------- /app/containers/HomePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Home Actions 3 | * 4 | * Actions change things in your application 5 | * Since this boilerplate uses a uni-directional data flow, specifically redux, 6 | * we have these actions which are the only way your application interacts with 7 | * your appliction state. This guarantees that your state is up to date and nobody 8 | * messes it up weirdly somewhere. 9 | * 10 | * To add a new Action: 11 | * 1) Import your constant 12 | * 2) Add a function like this: 13 | * export function yourAction(var) { 14 | * return { type: YOUR_ACTION_CONSTANT, var: var } 15 | * } 16 | */ 17 | 18 | import { 19 | CHANGE_USERNAME, 20 | } from './constants'; 21 | 22 | /** 23 | * Changes the input field of the form 24 | * 25 | * @param {name} name The new text of the input field 26 | * 27 | * @return {object} An action object with a type of CHANGE_USERNAME 28 | */ 29 | export function changeUsername(name) { 30 | return { 31 | type: CHANGE_USERNAME, 32 | name, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /app/components/Toggle/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import Toggle from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import { IntlProvider, defineMessages } from 'react-intl'; 6 | import React from 'react'; 7 | 8 | describe('', () => { 9 | it('should contain default text', () => { 10 | const defaultEnMessage = 'someContent'; 11 | const defaultDeMessage = 'someOtherContent'; 12 | const messages = defineMessages({ 13 | en: { 14 | id: 'app.components.LocaleToggle.en', 15 | defaultMessage: defaultEnMessage, 16 | }, 17 | de: { 18 | id: 'app.components.LocaleToggle.en', 19 | defaultMessage: defaultDeMessage, 20 | }, 21 | }); 22 | const renderedComponent = shallow( 23 | 24 | 25 | 26 | ); 27 | expect(renderedComponent.contains()).toEqual(true); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/css/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing CSS modules 2 | 3 | To remove this feature from your setup, stop importing `.css` files in your 4 | components and delete the `modules` option from the `css-loader` declaration in 5 | [`webpack.prod.babel.js`](/internals/webpack/webpack.prod.babel.js) and 6 | [`webpack.base.babel.js`](/internals/webpack/webpack.base.babel.js)! 7 | 8 | ## Removing PostCSS 9 | 10 | To remove PostCSS, delete the `postcssPlugins` option and remove all occurences 11 | of the `postcss-loader` from 12 | 13 | - [`webpack.dev.babel.js`](/internals/webpack/webpack.dev.babel.js) 14 | - [`webpack.prod.babel.js`](/internals/webpack/webpack.prod.babel.js) 15 | - [`webpack.base.babel.js`](/internals/webpack/webpack.base.babel.js) 16 | 17 | When that is done - and you've verified that everything is still working - remove 18 | all related dependencies from [`package.json`](/package.json)! 19 | 20 | ## Removing `sanitize.css` 21 | 22 | Delete [lines 44 and 45 in `app.js`](../../app/app.js#L44-L45) and remove it 23 | from the `dependencies` in [`package.json`](../../package.json)! 24 | -------------------------------------------------------------------------------- /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 | describe('', () => { 12 | it('should render its children', () => { 13 | const children = (

    Test

    ); 14 | const renderedComponent = shallow( 15 |
    16 | {children} 17 | 18 | ); 19 | expect(renderedComponent.contains(children)).toEqual(true); 20 | }); 21 | 22 | it('should adopt the className', () => { 23 | const renderedComponent = shallow(); 24 | expect(renderedComponent.find('a').hasClass('test')).toEqual(true); 25 | }); 26 | 27 | it('should adopt the href', () => { 28 | const renderedComponent = shallow(); 29 | expect(renderedComponent.prop('href')).toEqual('mxstbr.com'); 30 | }); 31 | 32 | it('should adopt the target', () => { 33 | const renderedComponent = shallow(); 34 | expect(renderedComponent.prop('target')).toEqual('_blank'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maximilian Stoiber 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. -------------------------------------------------------------------------------- /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 | startProjectHeader: { 10 | id: 'boilerplate.containers.HomePage.start_project.header', 11 | defaultMessage: 'Start your next react project in seconds', 12 | }, 13 | startProjectMessage: { 14 | id: 'boilerplate.containers.HomePage.start_project.message', 15 | defaultMessage: 'A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices', 16 | }, 17 | trymeHeader: { 18 | id: 'boilerplate.containers.HomePage.tryme.header', 19 | defaultMessage: 'Try me!', 20 | }, 21 | trymeMessage: { 22 | id: 'boilerplate.containers.HomePage.tryme.message', 23 | defaultMessage: 'Show Github repositories by', 24 | }, 25 | trymeAtPrefix: { 26 | id: 'boilerplate.containers.HomePage.tryme.atPrefix', 27 | defaultMessage: '@', 28 | }, 29 | featuresButton: { 30 | id: 'boilerplate.containers.HomePage.features.Button', 31 | defaultMessage: 'Features', 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /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 React from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { IntlProvider } from 'react-intl'; 13 | import { selectLocale } from './selectors'; 14 | 15 | export class LanguageProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 | 19 | {React.Children.only(this.props.children)} 20 | 21 | ); 22 | } 23 | } 24 | 25 | LanguageProvider.propTypes = { 26 | locale: React.PropTypes.string, 27 | messages: React.PropTypes.object, 28 | children: React.PropTypes.element.isRequired, 29 | }; 30 | 31 | const mapStateToProps = createSelector( 32 | selectLocale(), 33 | (locale) => ({ locale }) 34 | ); 35 | 36 | export default connect(mapStateToProps)(LanguageProvider); 37 | -------------------------------------------------------------------------------- /app/components/Footer/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { shallow } from 'enzyme'; 3 | import React from 'react'; 4 | import { FormattedMessage } from 'react-intl'; 5 | 6 | import messages from '../messages'; 7 | import Footer from '../index'; 8 | import A from 'components/A'; 9 | 10 | describe('
    97 | ); 98 | } 99 | } 100 | FeaturePage.propTypes = { 101 | changeRoute: React.PropTypes.func, 102 | }; 103 | 104 | function mapDispatchToProps(dispatch) { 105 | return { 106 | changeRoute: (url) => dispatch(push(url)), 107 | }; 108 | } 109 | 110 | export default connect(null, mapDispatchToProps)(FeaturePage); 111 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/sagas.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for HomePage sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | import { take, call, put, select, fork, cancel } from 'redux-saga/effects'; 7 | import { LOCATION_CHANGE } from 'react-router-redux'; 8 | 9 | import { getRepos, getReposWatcher, githubData } from '../sagas'; 10 | 11 | import { LOAD_REPOS } from 'containers/App/constants'; 12 | import { reposLoaded, repoLoadingError } from 'containers/App/actions'; 13 | 14 | import request from 'utils/request'; 15 | import { selectUsername } from 'containers/HomePage/selectors'; 16 | 17 | const username = 'mxstbr'; 18 | 19 | describe('getRepos Saga', () => { 20 | let getReposGenerator; 21 | 22 | // We have to test twice, once for a successful load and once for an unsuccessful one 23 | // so we do all the stuff that happens beforehand automatically in the beforeEach 24 | beforeEach(() => { 25 | getReposGenerator = getRepos(); 26 | 27 | const selectDescriptor = getReposGenerator.next().value; 28 | expect(selectDescriptor).toEqual(select(selectUsername())); 29 | 30 | const requestURL = `https://api.github.com/users/${username}/repos?type=all&sort=updated`; 31 | const callDescriptor = getReposGenerator.next(username).value; 32 | expect(callDescriptor).toEqual(call(request, requestURL)); 33 | }); 34 | 35 | it('should dispatch the reposLoaded action if it requests the data successfully', () => { 36 | const response = { 37 | data: [{ 38 | name: 'First repo', 39 | }, { 40 | name: 'Second repo', 41 | }], 42 | }; 43 | const putDescriptor = getReposGenerator.next(response).value; 44 | expect(putDescriptor).toEqual(put(reposLoaded(response.data, username))); 45 | }); 46 | 47 | it('should call the repoLoadingError action if the response errors', () => { 48 | const response = { 49 | err: 'Some error', 50 | }; 51 | const putDescriptor = getReposGenerator.next(response).value; 52 | expect(putDescriptor).toEqual(put(repoLoadingError(response.err))); 53 | }); 54 | }); 55 | 56 | describe('getReposWatcher Saga', () => { 57 | const getReposWatcherGenerator = getReposWatcher(); 58 | 59 | it('should watch for LOAD_REPOS action', () => { 60 | const takeDescriptor = getReposWatcherGenerator.next().value; 61 | expect(takeDescriptor).toEqual(take(LOAD_REPOS)); 62 | }); 63 | 64 | it('should invoke getRepos saga on actions', () => { 65 | const callDescriptor = getReposWatcherGenerator.next(put(LOAD_REPOS)).value; 66 | expect(callDescriptor).toEqual(call(getRepos)); 67 | }); 68 | }); 69 | 70 | describe('githubDataSaga Saga', () => { 71 | const githubDataSaga = githubData(); 72 | 73 | let forkDescriptor; 74 | 75 | it('should asyncronously fork getReposWatcher saga', () => { 76 | forkDescriptor = githubDataSaga.next(); 77 | expect(forkDescriptor.value).toEqual(fork(getReposWatcher)); 78 | }); 79 | 80 | it('should yield until LOCATION_CHANGE action', () => { 81 | const takeDescriptor = githubDataSaga.next(); 82 | expect(takeDescriptor.value).toEqual(take(LOCATION_CHANGE)); 83 | }); 84 | 85 | it('should finally cancel() the forked getReposWatcher saga', 86 | function* githubDataSagaCancellable() { 87 | // reuse open fork for more integrated approach 88 | forkDescriptor = githubDataSaga.next(put(LOCATION_CHANGE)); 89 | expect(forkDescriptor.value).toEqual(cancel(forkDescriptor)); 90 | } 91 | ); 92 | }); 93 | -------------------------------------------------------------------------------- /internals/templates/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 */ 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 */ 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 | // Create redux store with history 32 | // this uses the singleton browserHistory provided by react-router 33 | // Optionally, this could be changed to leverage a created history 34 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 35 | const initialState = {}; 36 | const store = configureStore(initialState, browserHistory); 37 | 38 | // Sync history and store, as the react-router-redux reducer 39 | // is under the non-default key ("routing"), selectLocationState 40 | // must be provided for resolving how to retrieve the "route" in the state 41 | import { selectLocationState } from 'containers/App/selectors'; 42 | const history = syncHistoryWithStore(browserHistory, store, { 43 | selectLocationState: selectLocationState(), 44 | }); 45 | 46 | // Set up the router, wrapping all Routes in the App component 47 | import App from 'containers/App'; 48 | import createRoutes from './routes'; 49 | const rootRoute = { 50 | component: App, 51 | childRoutes: createRoutes(store), 52 | }; 53 | 54 | 55 | const render = (translatedMessages) => { 56 | ReactDOM.render( 57 | 58 | 59 | 68 | 69 | , 70 | document.getElementById('app') 71 | ); 72 | }; 73 | 74 | 75 | // Hot reloadable translation json files 76 | if (module.hot) { 77 | // modules.hot.accept does not accept dynamic dependencies, 78 | // have to be constants at compile-time 79 | module.hot.accept('./i18n', () => { 80 | render(translationMessages); 81 | }); 82 | } 83 | 84 | // Chunked polyfill for browsers without Intl support 85 | if (!window.Intl) { 86 | Promise.all([ 87 | System.import('intl'), 88 | System.import('intl/locale-data/jsonp/en.js'), 89 | ]).then(() => render(translationMessages)); 90 | } else { 91 | render(translationMessages); 92 | } 93 | 94 | // Install ServiceWorker and AppCache in the end since 95 | // it's not most important operation and if main code fails, 96 | // we do not want it installed 97 | import { install } from 'offline-plugin/runtime'; 98 | install(); 99 | -------------------------------------------------------------------------------- /internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(' ✓'); 8 | callback(); 9 | } 10 | 11 | if (!which('git')) { 12 | echo('Sorry, this script requires git'); 13 | exit(1); 14 | } 15 | 16 | if (!test('-e', 'internals/templates')) { 17 | echo('The example is deleted already.'); 18 | exit(1); 19 | } 20 | 21 | process.stdout.write('Cleanup started...'); 22 | 23 | // Cleanup components folder 24 | rm('-rf', 'app/components/*'); 25 | 26 | // Cleanup containers folder 27 | rm('-rf', 'app/containers/*'); 28 | mkdir('-p', 'app/containers/App'); 29 | mkdir('-p', 'app/containers/NotFoundPage'); 30 | mkdir('-p', 'app/containers/HomePage'); 31 | cp('internals/templates/appContainer.js', 'app/containers/App/index.js'); 32 | cp('internals/templates/notFoundPage/notFoundPage.js', 'app/containers/NotFoundPage/index.js'); 33 | cp('internals/templates/notFoundPage/messages.js', 'app/containers/NotFoundPage/messages.js'); 34 | cp('internals/templates/homePage/homePage.js', 'app/containers/HomePage/index.js'); 35 | cp('internals/templates/homePage/messages.js', 'app/containers/HomePage/messages.js'); 36 | 37 | // Handle Translations 38 | mkdir('-p', 'app/translations'); 39 | cp('internals/templates/translations/en.json', 40 | 'app/translations/en.json'); 41 | 42 | // move i18n file 43 | cp('internals/templates/i18n.js', 44 | 'app/i18n.js'); 45 | 46 | // Copy LanguageProvider 47 | mkdir('-p', 'app/containers/LanguageProvider'); 48 | mkdir('-p', 'app/containers/LanguageProvider/tests'); 49 | cp('internals/templates/languageProvider/actions.js', 50 | 'app/containers/LanguageProvider/actions.js'); 51 | cp('internals/templates/languageProvider/constants.js', 52 | 'app/containers/LanguageProvider/constants.js'); 53 | cp('internals/templates/languageProvider/languageProvider.js', 54 | 'app/containers/LanguageProvider/index.js'); 55 | cp('internals/templates/languageProvider/reducer.js', 56 | 'app/containers/LanguageProvider/reducer.js'); 57 | cp('internals/templates/languageProvider/selectors.js', 58 | 'app/containers/LanguageProvider/selectors.js'); 59 | cp('internals/templates/styles.css', 'app/containers/App/styles.css'); 60 | 61 | // Copy selectors 62 | mkdir('app/containers/App/tests'); 63 | cp('internals/templates/selectors.js', 64 | 'app/containers/App/selectors.js'); 65 | cp('internals/templates/selectors.test.js', 66 | 'app/containers/App/tests/selectors.test.js'); 67 | 68 | // Utils 69 | rm('-rf', 'app/utils'); 70 | mkdir('app/utils'); 71 | mkdir('app/utils/tests'); 72 | cp('internals/templates/asyncInjectors.js', 73 | 'app/utils/asyncInjectors.js'); 74 | cp('internals/templates/asyncInjectors.test.js', 75 | 'app/utils/tests/asyncInjectors.test.js'); 76 | 77 | // Replace the files in the root app/ folder 78 | cp('internals/templates/app.js', 'app/app.js'); 79 | cp('internals/templates/index.html', 'app/index.html'); 80 | cp('internals/templates/reducers.js', 'app/reducers.js'); 81 | cp('internals/templates/routes.js', 'app/routes.js'); 82 | cp('internals/templates/store.js', 'app/store.js'); 83 | cp('internals/templates/store.test.js', 'app/tests/store.test.js'); 84 | 85 | // Remove the templates folder 86 | rm('-rf', 'internals/templates'); 87 | 88 | process.stdout.write(' ✓'); 89 | 90 | // Commit the changes 91 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) { 92 | echo('\nError: Git commit failed'); 93 | exit(1); 94 | } 95 | 96 | echo('\nCleanup done. Happy Coding!!!'); 97 | -------------------------------------------------------------------------------- /server/middlewares/api.js: -------------------------------------------------------------------------------- 1 | const low = require('lowdb'); 2 | const bodyParser = require('body-parser'); 3 | const uuid = require('uuid'); 4 | 5 | function setupDb() { 6 | const db = low(); 7 | 8 | db.defaults({ topics: [], links: [] }) 9 | .value(); 10 | 11 | const topic1 = { 12 | name: 'libraries', 13 | description: 'links to useful open source libraries', 14 | }; 15 | 16 | const topic2 = { 17 | name: 'apps', 18 | description: 'links to new and exciting apps', 19 | }; 20 | 21 | const topic3 = { 22 | name: 'news', 23 | description: 'links to programming related news articles', 24 | }; 25 | 26 | 27 | db.get('topics').push(topic1).value(); 28 | db.get('topics').push(topic2).value(); 29 | db.get('topics').push(topic3).value(); 30 | 31 | db.get('links').push({ 32 | description: 'The very library we are working with now', 33 | url: 'https://github.com/facebook/react', 34 | topicName: topic1.name, 35 | id: uuid(), 36 | voteCount: 0, 37 | voters: [], 38 | }).value(); 39 | db.get('links').push({ 40 | description: 'Some old videos', 41 | url: 'http://tagtree.io', 42 | topicName: topic1.name, 43 | id: uuid(), 44 | voteCount: 0, 45 | voters: [], 46 | }).value(); 47 | 48 | db.get('links').push({ 49 | description: 'An app to manage your finances', 50 | url: 'https://22seven.com', 51 | topicName: topic2.name, 52 | id: uuid(), 53 | voteCount: 0, 54 | voters: [], 55 | }).value(); 56 | db.get('links').push({ 57 | description: 'Go find some news yourself!', 58 | url: 'https://google.com', 59 | topicName: topic3.name, 60 | id: uuid(), 61 | voteCount: 0, 62 | voters: [], 63 | }).value(); 64 | 65 | return db; 66 | } 67 | 68 | module.exports = (app) => { 69 | const db = setupDb(); 70 | 71 | app.use((req, res, next) => { 72 | // Website you wish to allow to connect 73 | res.setHeader('Access-Control-Allow-Origin', '*'); 74 | // Request methods you wish to allow 75 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); 76 | // Request headers you wish to allow 77 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization'); 78 | 79 | // Pass to next layer of middleware 80 | next(); 81 | }); 82 | 83 | app.use(bodyParser.json()); 84 | 85 | app.get('/api/topics', (req, res) => { 86 | res.send(db.get('topics').toArray().value()); 87 | }); 88 | 89 | app.get('/api/topics/:name/links', (req, res) => { 90 | const links = db.get('links').filter((l) => 91 | l.topicName === req.params.name 92 | ).value(); 93 | res.send(links); 94 | }); 95 | 96 | app.post('/api/topics/:name/links', (req, res) => { 97 | const existingLink = db.get('links').find({ url: req.body.url }).value(); 98 | if (existingLink) { 99 | return res.send(403); 100 | } 101 | 102 | const link = Object.assign({}, req.body, { 103 | id: uuid(), 104 | voteCount: 0, 105 | voters: [], 106 | }); 107 | db.get('links').push(link).value(); 108 | return res.send(link); 109 | }); 110 | 111 | app.post('/api/links/:id/vote', (req, res) => { 112 | const link = db.get('links').find({ id: req.params.id }).value(); 113 | if (link.voters && link.voters.indexOf(req.body.email) > -1) { 114 | return res.send(403); 115 | } 116 | 117 | link.voters.push(req.body.email); 118 | link.voteCount += req.body.increment; 119 | return res.send(link); 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## RBP v3: The "JS Fatigue Antivenin" Edition 4 | 5 | React Boilerplate (RBP) v3.0.0 is out, and it's a _complete_ rewrite! :tada: 6 | 7 | We've focused on becoming a rock-solid foundation to start your next project 8 | with, no matter what its scale. You get to focus on writing your app because we 9 | focus on making that as easy as pie. 10 | 11 | website! 12 | 13 | ## Highlights 14 | 15 | - **Scaffolding**: Thanks to @somus, you can now run `npm run generate` in your 16 | terminal and immediately create new components, containers, sagas, routes and 17 | selectors! No more context switching, no more "Create new file, copy and paste 18 | that boilerplate structure, bla bla": just `npm run generate ` and go. 19 | 20 | Oh... and starting a project got a whole lot easier too: `npm run setup`. Done. 21 | 22 | - **Revamped architecture**: Following the incredible discussion in #27 (thanks 23 | everybody for sharing your thoughts), we now have a weapons-grade, domain-driven 24 | application architecture. 25 | 26 | "Smart" containers are now isolated from stateless and/or generic components, 27 | tests are now co-located with the code that they validate. 28 | 29 | - **New industry-standard JS utilties** We're now making the most of... 30 | - ImmutableJS 31 | - reselect 32 | - react-router-redux 33 | - redux-saga 34 | 35 | - **Huge CSS Improvements** 36 | - _[CSS Modules](docs/css/css-modules.md)_: Finally, truly modular, reusable 37 | styles! 38 | - _Page-specific CSS_: smart Webpack configuration means that only the CSS 39 | your components need is served 40 | - _Standards rock:_ Nothing beats consistent styling so we beefed up the 41 | quality checks with **[stylelint](docs/css/stylelint.md)** to help ensure 42 | that you and your team stay on point. 43 | 44 | - **Performance** 45 | - _Code splitting_: splitting/chunking by route means the leanest, meanest 46 | payload (because the fastest code is the code you don't load!) 47 | - _PageSpeed Metrics_ are built right in with `npm run pagespeed` 48 | 49 | - **Testing setup**: Thanks to @jbinto's herculean efforts, testing is now a 50 | first-class citizen of this boilerplate. (the example app has _99% test coverage!_) 51 | Karma and enzyme take care of unit testing, while ngrok tunnels your local 52 | server for access from anywhere in the world – perfect for testing on different 53 | devices in different locations. 54 | 55 | - **New server setup**: Thanks to the mighty @grabbou, we now use express.js to 56 | give users a production-ready server right out of the box. Hot reloading is 57 | still as available as always, but adding a custom API or a non-React page to 58 | your application is now easier than ever :smile: 59 | 60 | - **Cleaner layout:** We've taken no prisoners with our approach to keeping your 61 | code the star of the show: wherever possible, the new file layout keeps the 62 | config in the background so that you can keep your focus where it needs to be. 63 | 64 | - **Documentation**: Thanks to @oliverturner, this boilerplate has some of the best 65 | documentation going. Not just clearly explained usage guides, but easy-to-follow 66 | _removal_ guides for most features too. RBP is just a launchpad: don't want to 67 | use a bundled feature? Get rid of it quickly and easily without having to dig 68 | through the code. 69 | 70 | - **Countless small improvements**: Everything, from linting pre-commit (thanks 71 | @okonet!) to code splitting to cross-OS compatibility is now tested and ready 72 | to go: 73 | 74 | - We finally added a **[CoC](CODE_OF_CONDUCT.md)** 75 | - Windows compatibility has improved massively 76 | --------------------------------------------------------------------------------