├── internals
├── mocks
│ └── image.js
├── testing
│ └── test-bundler.js
├── webpack
│ ├── webpack.dll.babel.js
│ ├── webpack.prod.babel.js
│ ├── webpack.base.babel.js
│ └── webpack.dev.babel.js
├── scripts
│ └── dependencies.js
└── config.js
├── .coveralls.yml
├── app
├── translations
│ ├── en.js
│ ├── de.js
│ ├── en.json
│ └── de.json
├── store
│ ├── nav
│ │ ├── constants.js
│ │ ├── actions.js
│ │ ├── selectors.js
│ │ └── reducer.js
│ ├── language
│ │ ├── constants.js
│ │ ├── actions.js
│ │ ├── selectors.test.js
│ │ ├── selectors.js
│ │ ├── actions.test.js
│ │ ├── reducer.test.js
│ │ └── reducer.js
│ └── auth
│ │ ├── actions.js
│ │ ├── selectors.js
│ │ ├── reducer.js
│ │ └── constants.js
├── pages
│ ├── FeatureList
│ │ ├── Feature
│ │ │ ├── styles.scss
│ │ │ ├── index.jsx
│ │ │ ├── index.story.js
│ │ │ └── index.test.js
│ │ ├── styles.scss
│ │ ├── index.story.js
│ │ ├── index.spec.js
│ │ ├── index.jsx
│ │ ├── index.test.js
│ │ ├── __snapshots__
│ │ │ └── index.spec.js.snap
│ │ └── messages.js
│ ├── SignIn
│ │ ├── messages.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── SignUp
│ │ ├── messages.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── Home
│ │ ├── RepositoriesList
│ │ │ ├── Repository
│ │ │ │ ├── styles.scss
│ │ │ │ ├── RepositoryLink
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── IssuesLink
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── IssueIcon
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── index.test.js
│ │ │ │ └── index.jsx
│ │ │ ├── messages.js
│ │ │ ├── styles.scss
│ │ │ └── index.jsx
│ │ ├── styles.scss
│ │ ├── messages.js
│ │ └── index.jsx
│ ├── SignOut
│ │ ├── messages.js
│ │ ├── index.story.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ └── 404-NotFound
│ │ ├── messages.js
│ │ ├── index.jsx
│ │ └── index.test.js
├── images
│ ├── favicon.ico
│ ├── icon-72x72.png
│ ├── icon-96x96.png
│ ├── icon-120x120.png
│ ├── icon-128x128.png
│ ├── icon-144x144.png
│ ├── icon-152x152.png
│ ├── icon-167x167.png
│ ├── icon-180x180.png
│ ├── icon-192x192.png
│ ├── icon-384x384.png
│ └── icon-512x512.png
├── atoms
│ ├── H2
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── H1
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── Error
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── A
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── List
│ │ ├── index.story.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ └── Img
│ │ ├── index.jsx
│ │ └── index.test.js
├── molecules
│ ├── Helmet
│ │ ├── index.js
│ │ ├── TitleIntl
│ │ │ ├── index.jsx
│ │ │ ├── Title
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.test.js
│ │ │ └── index.test.js
│ │ └── MetaIntl
│ │ │ ├── Meta
│ │ │ ├── index.jsx
│ │ │ └── index.test.js
│ │ │ ├── index.jsx
│ │ │ └── index.test.js
│ ├── Dropdown
│ │ ├── Option
│ │ │ ├── index.jsx
│ │ │ └── index.test.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ └── Page
│ │ └── index.jsx
├── utils
│ ├── store
│ │ └── index.js
│ ├── authenticated
│ │ └── index.js
│ ├── yup
│ │ └── index.js
│ ├── test
│ │ └── index.js
│ ├── auth
│ │ ├── index.js
│ │ └── index.test.js
│ ├── form
│ │ └── index.js
│ └── request
│ │ ├── index.js
│ │ ├── base
│ │ ├── index.js
│ │ └── index.test.js
│ │ └── index.test.js
├── reducers.js
├── organisms
│ ├── App
│ │ ├── defaultTheme
│ │ │ └── index.js
│ │ ├── global-styles.js
│ │ ├── AuthorizedRoute
│ │ │ ├── index.jsx
│ │ │ └── index.test.js
│ │ ├── UnauthorizedRoute
│ │ │ ├── index.jsx
│ │ │ └── index.test.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── Footer
│ │ ├── messages.js
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── LocaleSwitcher
│ │ ├── messages.js
│ │ ├── index.test.js
│ │ └── index.jsx
│ ├── Header
│ │ ├── LinksBlock
│ │ │ ├── messages.js
│ │ │ └── index.jsx
│ │ ├── index.story.js
│ │ └── index.jsx
│ ├── LanguageProvider
│ │ ├── index.jsx
│ │ └── index.test.js
│ ├── SignInForm
│ │ ├── index.story.js
│ │ ├── messages.js
│ │ └── index.jsx
│ └── SignUpForm
│ │ ├── index.story.js
│ │ ├── messages.js
│ │ └── index.jsx
├── i18n
│ └── index.js
├── templates
│ └── default
│ │ └── index.jsx
├── manifest.json
├── index.html
├── .htaccess
├── .nginx.conf
└── app.js
├── .env
├── server
├── argv.js
├── port.js
├── middlewares
│ ├── frontendMiddleware.js
│ ├── addProdMiddlewares.js
│ └── addDevMiddlewares.js
├── logger.js
└── index.js
├── postcss.config.js
├── .gitignore
├── .storybook
├── webpack.config.js
└── config.js
├── .editorconfig
├── .travis.yml
├── .babelrc
├── LICENSE.md
├── jest.config.js
├── .gitattributes
├── .eslintrc
├── README.md
└── package.json
/internals/mocks/image.js:
--------------------------------------------------------------------------------
1 | module.exports = 'IMAGE_MOCK';
2 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: rfFLCXbO6A1d8ndBOBjHFwySsLtsLRxNS
2 |
--------------------------------------------------------------------------------
/app/translations/en.js:
--------------------------------------------------------------------------------
1 | export default from 'yup/lib/locale';
2 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | APP_ENV_API_BASE_URL = "http://localhost:8000"
2 | APP_ENV_BASE_PATH = "/"
3 |
--------------------------------------------------------------------------------
/server/argv.js:
--------------------------------------------------------------------------------
1 | module.exports = require('minimist')(process.argv.slice(2));
2 |
--------------------------------------------------------------------------------
/app/store/nav/constants.js:
--------------------------------------------------------------------------------
1 | export const SET_PAGE_TITLE = 'actions/nav/SET_PAGE_TITLE';
2 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/Feature/styles.scss:
--------------------------------------------------------------------------------
1 | .feature-header {
2 | font-weight: bold;
3 | }
4 |
--------------------------------------------------------------------------------
/app/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/favicon.ico
--------------------------------------------------------------------------------
/app/images/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-72x72.png
--------------------------------------------------------------------------------
/app/images/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-96x96.png
--------------------------------------------------------------------------------
/app/images/icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-120x120.png
--------------------------------------------------------------------------------
/app/images/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-128x128.png
--------------------------------------------------------------------------------
/app/images/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-144x144.png
--------------------------------------------------------------------------------
/app/images/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-152x152.png
--------------------------------------------------------------------------------
/app/images/icon-167x167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-167x167.png
--------------------------------------------------------------------------------
/app/images/icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-180x180.png
--------------------------------------------------------------------------------
/app/images/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-192x192.png
--------------------------------------------------------------------------------
/app/images/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-384x384.png
--------------------------------------------------------------------------------
/app/images/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexander-elgin/atomic-react-redux/HEAD/app/images/icon-512x512.png
--------------------------------------------------------------------------------
/internals/testing/test-bundler.js:
--------------------------------------------------------------------------------
1 | // needed for regenerator-runtime
2 | // (ES7 generator support)
3 | import 'babel-polyfill';
4 |
--------------------------------------------------------------------------------
/server/port.js:
--------------------------------------------------------------------------------
1 | const argv = require('./argv');
2 |
3 | module.exports = parseInt(argv.port || process.env.PORT || '3000', 10);
4 |
--------------------------------------------------------------------------------
/app/store/language/constants.js:
--------------------------------------------------------------------------------
1 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE';
2 | export const DEFAULT_LOCALE = 'en';
3 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | precss: {},
4 | autoprefixer: {},
5 | cssnano: {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/app/atoms/H2/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const H2 = styled.h2`
4 | font-size: 1.5em;
5 | `;
6 |
7 | export default H2;
8 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/index.js:
--------------------------------------------------------------------------------
1 | import TitleIntl from './TitleIntl';
2 | import MetaIntl from './MetaIntl';
3 |
4 | export {
5 | MetaIntl,
6 | TitleIntl,
7 | };
8 |
--------------------------------------------------------------------------------
/app/atoms/H1/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const H1 = styled.h1`
4 | font-size: 2em;
5 | margin-bottom: 0.25em;
6 | `;
7 |
8 | export default H1;
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Don't check auto-generated stuff into git
2 | coverage
3 | build
4 | node_modules
5 |
6 | # Cruft
7 | .DS_Store
8 | npm-debug.log
9 | yarn-error.log
10 | .idea
11 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/styles.scss:
--------------------------------------------------------------------------------
1 | .list {
2 | font-family: Georgia, Times, 'Times New Roman', serif;
3 | padding-left: 1.75em;
4 |
5 | li {
6 | margin: 1em 0;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/atoms/Error/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Error = styled.div`
4 | color: rgb(244, 67, 54);
5 | font-size: 12px;
6 | `;
7 |
8 | export default Error;
9 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const config = require('../internals/webpack/webpack.base.babel')({plugins: []});
2 |
3 | module.exports = {
4 | module: {
5 | rules: config.module.rules,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/app/atoms/A/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const A = styled.a`
4 | color: #41addd;
5 |
6 | &:hover {
7 | color: #6cc0e5;
8 | }
9 | `;
10 |
11 | export default A;
12 |
--------------------------------------------------------------------------------
/app/store/nav/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | SET_PAGE_TITLE,
3 | } from './constants';
4 |
5 | export function setPageTitle(payload) {
6 | return {
7 | type: SET_PAGE_TITLE,
8 | payload,
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/app/pages/SignIn/messages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | export default defineMessages({
4 | metaTitle: {
5 | id: 'boilerplate.shared.Auth.signIn',
6 | defaultMessage: 'Sign in',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/app/pages/SignUp/messages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | export default defineMessages({
4 | metaTitle: {
5 | id: 'boilerplate.shared.Auth.signUp',
6 | defaultMessage: 'Sign up',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/app/atoms/List/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import List from './';
5 |
6 | storiesOf('List', module)
7 | .add('default', () =>
)
8 | ;
9 |
--------------------------------------------------------------------------------
/app/utils/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import { fromJS } from 'immutable';
3 | import { combineReducers } from 'redux-immutable';
4 |
5 | export default (reducers, initialState = {}) => createStore(combineReducers(reducers), fromJS(initialState));
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/app/reducers.js:
--------------------------------------------------------------------------------
1 | const context = require.context('./store', true, /reducer\.js$/);
2 |
3 | export default context.keys().reduce((reducers, fileRelativePath) => Object.assign(reducers, {
4 | [fileRelativePath.replace('./', '').replace('/reducer.js', '')]: context(fileRelativePath).default,
5 | }), {});
6 |
--------------------------------------------------------------------------------
/app/store/language/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 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/Repository/styles.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | max-height: 30em;
8 | overflow-y: auto;
9 | }
10 |
11 | .icon {
12 | fill: #ccc;
13 | margin-right: 0.25em;
14 | }
15 |
--------------------------------------------------------------------------------
/app/store/auth/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | SIGN_IN_SUCCESS,
3 | SIGN_OUT,
4 | } from './constants';
5 |
6 | export function setSignInData(user) {
7 | return {
8 | type: SIGN_IN_SUCCESS,
9 | user,
10 | };
11 | }
12 |
13 | export function signOut() {
14 | return {
15 | type: SIGN_OUT,
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/app/store/nav/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const selectNav = (state) => state.get('nav');
4 |
5 | const makeSelectPageTitle = () => createSelector(
6 | selectNav,
7 | (globalState) => globalState.get('pageTitle')
8 | );
9 |
10 | export {
11 | selectNav,
12 | makeSelectPageTitle,
13 | };
14 |
--------------------------------------------------------------------------------
/app/organisms/App/defaultTheme/index.js:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles';
2 |
3 | const defaultTheme = createMuiTheme({
4 | typography: {
5 | useNextVariants: true,
6 | },
7 | palette: {
8 | primary: {
9 | 500: '#41ADDD',
10 | },
11 | },
12 | });
13 |
14 | export default defaultTheme;
15 |
--------------------------------------------------------------------------------
/app/atoms/List/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const List = ({ items, ...rest }) => (
5 |
6 | {items.map((item, key) => {item} )}
7 |
8 | );
9 |
10 | List.propTypes = {
11 | items: PropTypes.array.isRequired,
12 | };
13 |
14 | export default List;
15 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/TitleIntl/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | import Title from './Title';
5 |
6 | const TitleIntl = (props) => (
7 |
8 | {(title) => }
9 |
10 | );
11 |
12 | export default TitleIntl;
13 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/Repository/RepositoryLink/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import NormalA from '../../../../../atoms/A';
3 |
4 | const RepoLink = styled(NormalA)`
5 | height: 100%;
6 | color: black;
7 | display: flex;
8 | align-items: center;
9 | width: 100%;
10 | `;
11 |
12 | export default RepoLink;
13 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import { configure } from '@storybook/react';
3 |
4 | function requireAll(requireContext) {
5 | return requireContext.keys().map(requireContext);
6 | }
7 |
8 | function loadStories() {
9 | requireAll(require.context('../app', true, /(story|stories).jsx?$/));
10 | }
11 |
12 | configure(loadStories, module);
13 |
--------------------------------------------------------------------------------
/app/pages/SignOut/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 | signOutMessage: {
10 | id: 'boilerplate.pages.SignOut.signOutMessage',
11 | defaultMessage: 'Signing out',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/app/pages/404-NotFound/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.pages.NotFound.header',
11 | defaultMessage: 'Page not found.',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/TitleIntl/Title/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Helmet } from 'react-helmet';
3 | import PropTypes from 'prop-types';
4 |
5 | const Title = ({ content }) => (
6 |
7 | { content }
8 |
9 | );
10 |
11 | Title.propTypes = {
12 | content: PropTypes.string,
13 | };
14 |
15 | export default Title;
16 |
--------------------------------------------------------------------------------
/app/pages/SignOut/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import { IntlProvider } from 'react-intl';
5 |
6 | import SignOut from './';
7 |
8 | storiesOf('Sign Out', module)
9 | .add('default', () => (
10 |
11 | {}} />
12 |
13 | ))
14 | ;
15 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/Repository/IssuesLink/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import NormalA from '../../../../../atoms/A';
3 |
4 | const IssuesLink = styled(NormalA)`
5 | height: 100%;
6 | color: black;
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | white-space: nowrap;
11 | `;
12 |
13 | export default IssuesLink;
14 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/MetaIntl/Meta/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Helmet } from 'react-helmet';
4 |
5 | const Meta = ({ content, name }) => (
6 |
7 |
8 |
9 | );
10 |
11 | Meta.propTypes = {
12 | content: PropTypes.string,
13 | name: PropTypes.string,
14 | };
15 |
16 | export default Meta;
17 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/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 | somethingWrong: {
10 | id: 'boilerplate.pages.Home.RepositoriesList.somethingWrong',
11 | defaultMessage: 'Something went wrong, please try again!',
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import { IntlProvider } from 'react-intl';
5 |
6 | import FeatureListPage from './';
7 |
8 | storiesOf('FeatureListPage', module)
9 | .addDecorator((story) => (
10 |
11 | {story()}
12 |
13 | ))
14 | .add('default', () => )
15 | ;
16 |
--------------------------------------------------------------------------------
/app/pages/Home/styles.scss:
--------------------------------------------------------------------------------
1 | .form {
2 | margin-bottom: 1em;
3 | }
4 |
5 | .input {
6 | background: transparent;
7 | border: none;
8 | outline: none;
9 | border-bottom: 1px dotted #999;
10 | margin-left: 0.4em;
11 | }
12 |
13 | .section {
14 | margin: 3em auto;
15 |
16 | &:first-child {
17 | margin-top: 0;
18 | }
19 | }
20 |
21 | .centered-section {
22 | composes: section;
23 | text-align: center;
24 | }
25 |
--------------------------------------------------------------------------------
/app/store/language/selectors.test.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import {
4 | selectLanguage,
5 | } from './selectors';
6 |
7 | describe('selectLanguage', () => {
8 | it('should select the global state', () => {
9 | const globalState = fromJS({});
10 | const mockedState = fromJS({
11 | language: globalState,
12 | });
13 | expect(selectLanguage(mockedState)).toEqual(globalState);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/app/pages/SignIn/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Page from '../../molecules/Page';
4 | import SignInForm from '../../organisms/SignInForm';
5 | import messages from './messages';
6 |
7 | const SignInPage = () => (
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default SignInPage;
16 |
--------------------------------------------------------------------------------
/app/pages/SignUp/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Page from '../../molecules/Page';
4 | import SignUpForm from '../../organisms/SignUpForm';
5 | import messages from './messages';
6 |
7 | const SignUpPage = () => (
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default SignUpPage;
16 |
--------------------------------------------------------------------------------
/app/organisms/Footer/messages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | export default defineMessages({
4 | licenseMessage: {
5 | id: 'boilerplate.organisms.Footer.license.message',
6 | defaultMessage: 'This project is licensed under the MIT license.',
7 | },
8 | authorMessage: {
9 | id: 'boilerplate.organisms.Footer.author.message',
10 | defaultMessage: `
11 | Made with love by {author}.
12 | `,
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/app/utils/authenticated/index.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { createStructuredSelector } from 'reselect';
3 |
4 | import { makeSelectIsAuthenticated } from '../../store/auth/selectors';
5 |
6 | const mapStateToProps = createStructuredSelector({
7 | authenticated: makeSelectIsAuthenticated(),
8 | });
9 |
10 | const setAuthenticatedProp = (Component) => connect(mapStateToProps, null)(Component);
11 | export default setAuthenticatedProp;
12 |
--------------------------------------------------------------------------------
/app/organisms/LocaleSwitcher/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 |
8 | export default defineMessages({
9 | en: {
10 | id: 'boilerplate.organisms.LocaleSwitcher.en',
11 | defaultMessage: 'en',
12 | },
13 | de: {
14 | id: 'boilerplate.organisms.LocaleSwitcher.de',
15 | defaultMessage: 'de',
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/MetaIntl/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | import Meta from './Meta';
6 |
7 | const MetaIntl = ({ name, ...rest }) => (
8 |
9 | {(content) => }
10 |
11 | );
12 |
13 | MetaIntl.propTypes = {
14 | name: PropTypes.string,
15 | };
16 |
17 | export default MetaIntl;
18 |
--------------------------------------------------------------------------------
/app/store/language/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 makeSelectLocale = () => createSelector(
13 | selectLanguage,
14 | (languageState) => languageState.get('locale')
15 | );
16 |
17 | export {
18 | selectLanguage,
19 | makeSelectLocale,
20 | };
21 |
--------------------------------------------------------------------------------
/app/i18n/index.js:
--------------------------------------------------------------------------------
1 | import { addLocaleData } from 'react-intl';
2 |
3 | const context = require.context('../translations', true, /\.json$/);
4 |
5 | export default context.keys().reduce((acc, fileRelativePath) => {
6 | const langIso2Code = fileRelativePath.replace('./', '').replace('.json', '');
7 | addLocaleData(require(`react-intl/locale-data/${langIso2Code}`)); // eslint-disable-line global-require
8 | acc[langIso2Code] = context(fileRelativePath);
9 | return acc;
10 | }, {});
11 |
--------------------------------------------------------------------------------
/app/store/auth/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const selectAuth = (state) => state.get('auth');
4 |
5 | const makeSelectUser = () => createSelector(
6 | selectAuth,
7 | (authState) => authState.get('user')
8 | );
9 |
10 | const makeSelectIsAuthenticated = () => createSelector(
11 | makeSelectUser(),
12 | (userState) => userState !== null
13 | );
14 |
15 | export {
16 | selectAuth,
17 | makeSelectUser,
18 | makeSelectIsAuthenticated,
19 | };
20 |
--------------------------------------------------------------------------------
/app/store/nav/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import {
4 | SET_PAGE_TITLE,
5 | } from './constants';
6 |
7 | const initialState = fromJS({
8 | pageTitle: '',
9 | });
10 |
11 | function navReducer(state = initialState, action = {}) {
12 | switch (action.type) {
13 | case SET_PAGE_TITLE:
14 | return state
15 | .set('pageTitle', action.payload);
16 | default:
17 | return state;
18 | }
19 | }
20 |
21 | export default navReducer;
22 |
--------------------------------------------------------------------------------
/app/store/language/actions.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | changeLocale,
3 | } from './actions';
4 |
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/molecules/Dropdown/Option/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { injectIntl, intlShape } from 'react-intl';
4 |
5 | const Option = ({ value, message, intl }) => (
6 |
7 | {message ? intl.formatMessage(message) : value}
8 |
9 | );
10 |
11 | Option.propTypes = {
12 | value: PropTypes.string.isRequired,
13 | message: PropTypes.object,
14 | intl: intlShape.isRequired,
15 | };
16 |
17 | export default injectIntl(Option);
18 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { IntlProvider } from 'react-intl';
5 |
6 | import FeatureListPage from './';
7 |
8 | describe(' ', () => {
9 | it('renders the Features List page', () => {
10 | expect(
11 | renderer.create(
12 |
13 |
14 |
15 | ).toJSON()
16 | ).toMatchSnapshot();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/app/store/auth/reducer.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import {
4 | SIGN_IN_SUCCESS,
5 | SIGN_OUT,
6 | } from './constants';
7 |
8 | const initialState = fromJS({
9 | user: null,
10 | });
11 |
12 | function reducer(state = initialState, action = {}) {
13 | switch (action.type) {
14 | case SIGN_IN_SUCCESS:
15 | return state.set('user', action.user);
16 | case SIGN_OUT:
17 | return state.set('user', null);
18 | default:
19 | return state;
20 | }
21 | }
22 |
23 | export default reducer;
24 |
--------------------------------------------------------------------------------
/app/atoms/Error/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import Error from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | const children = (Test );
10 | const renderComponent = (props = {}) => shallow(
11 |
12 | {children}
13 |
14 | );
15 |
16 | describe(' ', () => {
17 | it('renders children', () => expect(renderComponent().contains(children)).toBe(true));
18 | });
19 |
--------------------------------------------------------------------------------
/app/store/auth/constants.js:
--------------------------------------------------------------------------------
1 | export const SIGN_IN_SUCCESS = 'actions/auth/SIGN_IN_SUCCESS';
2 | export const SIGN_OUT = 'actions/auth/SIGN_OUT';
3 |
4 | export const ERRORS_PREFIX = 'boilerplate.shared.Auth.errors';
5 |
6 | export const DUPLICATED_EMAIL = 'DUPLICATED_EMAIL';
7 | export const EMPTY_PASSWORD = 'EMPTY_PASSWORD';
8 | export const INCORRECT_CREDENTIALS = 'INCORRECT_CREDENTIALS';
9 | export const INVALID_EMAIL = 'INVALID_EMAIL';
10 | export const INVALID_NAME = 'INVALID_NAME';
11 | export const INVALID_PASSWORD = 'INVALID_PASSWORD';
12 |
--------------------------------------------------------------------------------
/app/atoms/A/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import A from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | const children = (Test );
10 | const renderComponent = (props = {}) => shallow(
11 |
12 | {children}
13 |
14 | );
15 |
16 | describe(' ', () => {
17 | it('renders children', () => expect(renderComponent().contains(children)).toEqual(true));
18 | });
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | dist: trusty
3 | node_js:
4 | - "8.9.1"
5 | sudo: true
6 | git:
7 | depth: 1
8 |
9 | before_install:
10 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.9.4
11 | - export PATH="$HOME/.yarn/bin:$PATH"
12 | - yarn cache clean
13 | - yarn
14 | branches:
15 | only:
16 | - develop
17 | jobs:
18 | include:
19 | - stage: test
20 | script:
21 | - yarn test
22 | after_success:
23 | - yarn coveralls
24 | - stage: build
25 | script:
26 | - yarn build
27 |
--------------------------------------------------------------------------------
/app/pages/SignIn/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import SignInForm from '../../organisms/SignInForm';
6 |
7 | import SignInPage from './';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
11 | describe(' ', () => {
12 | let component;
13 |
14 | beforeEach(() => {
15 | component = shallow(
16 |
17 | );
18 | });
19 |
20 | it('renders the form', () => expect(component.type()).toEqual(SignInForm));
21 | });
22 |
--------------------------------------------------------------------------------
/app/pages/SignUp/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import SignUpForm from '../../organisms/SignUpForm';
6 |
7 | import SignUpPage from './';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
11 | describe(' ', () => {
12 | let component;
13 |
14 | beforeEach(() => {
15 | component = shallow(
16 |
17 | );
18 | });
19 |
20 | it('renders the form', () => expect(component.type()).toEqual(SignUpForm));
21 | });
22 |
--------------------------------------------------------------------------------
/app/pages/404-NotFound/index.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * NotFoundPage
3 | *
4 | * This is the page we show when the user visits a url that doesn't have a route
5 | */
6 |
7 | import React from 'react';
8 | import { FormattedMessage } from 'react-intl';
9 |
10 | import H1 from '../../atoms/H1';
11 | import Page from '../../molecules/Page';
12 | import messages from './messages';
13 |
14 | export default function NotFound() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/app/utils/yup/index.js:
--------------------------------------------------------------------------------
1 | import { setLocale } from 'yup';
2 |
3 | const context = require.context('../../translations', true, /\.js$/);
4 |
5 | const serializedLocales = JSON.stringify(context.keys().reduce((locales, fileRelativePath) => Object.assign(locales, {
6 | [fileRelativePath.replace('./', '').replace('.js', '')]: context(fileRelativePath).default,
7 | }), {}));
8 |
9 | // setLocale modifies the argument. That's the only way I found to fix the issue
10 | const setYupLocale = (langIso2Code) => setLocale(JSON.parse(serializedLocales)[langIso2Code]);
11 | export default setYupLocale;
12 |
--------------------------------------------------------------------------------
/app/store/language/reducer.test.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from 'immutable';
2 |
3 | import languageProviderReducer from './reducer';
4 | import {
5 | CHANGE_LOCALE,
6 | } from './constants';
7 |
8 | describe('languageProviderReducer', () => {
9 | it('returns the initial state', () => {
10 | expect(languageProviderReducer()).toEqual(fromJS({
11 | locale: 'en',
12 | }));
13 | });
14 |
15 | it('changes the locale', () => {
16 | expect(languageProviderReducer(undefined, { type: CHANGE_LOCALE, locale: 'de' }).toJS()).toEqual({
17 | locale: 'de',
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/app/utils/test/index.js:
--------------------------------------------------------------------------------
1 | import { FormattedMessage, IntlProvider } from 'react-intl';
2 |
3 | export function checkTranslation(message, id, defaultText) {
4 | expect(message.type()).toEqual(FormattedMessage);
5 | expect(message.prop('id')).toBe(id);
6 | expect(message.prop('defaultMessage')).toBe(defaultText);
7 | }
8 |
9 | export function getIntl(translate = (messageData) => messageData.defaultMessage) {
10 | const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
11 | const { intl } = intlProvider.getChildContext();
12 | intl.formatMessage = translate;
13 | return intl;
14 | }
15 |
--------------------------------------------------------------------------------
/app/store/language/reducer.js:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * LanguageProvider reducer
4 | *
5 | */
6 |
7 | import { fromJS } from 'immutable';
8 |
9 | import {
10 | CHANGE_LOCALE,
11 | DEFAULT_LOCALE,
12 | } from './constants';
13 |
14 | const initialState = fromJS({
15 | locale: DEFAULT_LOCALE,
16 | });
17 |
18 | function languageProviderReducer(state = initialState, action = {}) {
19 | switch (action.type) {
20 | case CHANGE_LOCALE:
21 | return state
22 | .set('locale', action.locale);
23 | default:
24 | return state;
25 | }
26 | }
27 |
28 | export default languageProviderReducer;
29 |
--------------------------------------------------------------------------------
/server/middlewares/frontendMiddleware.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | /**
4 | * Front-end middleware
5 | */
6 | module.exports = (app, options) => {
7 | const isProd = process.env.NODE_ENV === 'production';
8 |
9 | if (isProd) {
10 | const addProdMiddlewares = require('./addProdMiddlewares');
11 | addProdMiddlewares(app, options);
12 | } else {
13 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel');
14 | const addDevMiddlewares = require('./addDevMiddlewares');
15 | addDevMiddlewares(app, webpackConfig);
16 | }
17 |
18 | return app;
19 | };
20 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/Repository/IssueIcon/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | function IssueIcon(props) {
5 | return (
6 |
11 |
12 |
13 | );
14 | }
15 |
16 | IssueIcon.propTypes = {
17 | className: PropTypes.string,
18 | };
19 |
20 | export default IssueIcon;
21 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/Feature/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | import styles from './styles.scss';
6 |
7 | const Feature = ({ description, header }) => (
8 |
12 | );
13 |
14 | Feature.propTypes = {
15 | description: PropTypes.object.isRequired,
16 | header: PropTypes.object.isRequired,
17 | };
18 |
19 | export default Feature;
20 |
--------------------------------------------------------------------------------
/app/atoms/Img/index.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Img.js
4 | *
5 | * Renders an image, enforcing the usage of the alt="" tag
6 | */
7 |
8 | import React from 'react';
9 | import PropTypes from 'prop-types';
10 |
11 | function Img(props) {
12 | return (
13 |
14 | );
15 | }
16 |
17 | // We require the use of src and alt, only enforced by react in dev mode
18 | Img.propTypes = {
19 | src: PropTypes.oneOfType([
20 | PropTypes.string,
21 | PropTypes.object,
22 | ]).isRequired,
23 | alt: PropTypes.string.isRequired,
24 | className: PropTypes.string,
25 | };
26 |
27 | export default Img;
28 |
--------------------------------------------------------------------------------
/app/organisms/App/global-styles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | export default createGlobalStyle`
4 | html,
5 | body {
6 | height: 100%;
7 | width: 100%;
8 | }
9 |
10 | body {
11 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
12 | }
13 |
14 | body.fontLoaded {
15 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
16 | }
17 |
18 | #app {
19 | background-color: #fafafa;
20 | min-height: 100%;
21 | min-width: 100%;
22 | }
23 |
24 | p,
25 | label {
26 | font-family: Georgia, Times, 'Times New Roman', serif;
27 | line-height: 1.5em;
28 | }
29 | `;
30 |
--------------------------------------------------------------------------------
/app/organisms/Header/LinksBlock/messages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | export default defineMessages({
4 | features: {
5 | id: 'boilerplate.organisms.Header.features',
6 | defaultMessage: 'Features',
7 | },
8 | home: {
9 | id: 'boilerplate.organisms.Header.home',
10 | defaultMessage: 'Home',
11 | },
12 | signIn: {
13 | id: 'boilerplate.shared.Auth.signIn',
14 | defaultMessage: 'Sign in',
15 | },
16 | signOut: {
17 | id: 'boilerplate.shared.Auth.signOut',
18 | defaultMessage: 'Sign out',
19 | },
20 | signUp: {
21 | id: 'boilerplate.shared.Auth.signUp',
22 | defaultMessage: 'Sign up',
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/styles.scss:
--------------------------------------------------------------------------------
1 | .centered {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | }
6 |
7 | .list {
8 | list-style: none;
9 | margin: 0;
10 | width: 100%;
11 | padding: 0 1em;
12 |
13 | li {
14 | width: 100%;
15 | height: 3em;
16 | display: flex;
17 | align-items: center;
18 | position: relative;
19 | border-top: 1px solid #eee;
20 |
21 | &:first-child {
22 | border-top: none;
23 | }
24 | }
25 | }
26 |
27 | .wrapper {
28 | padding: 0;
29 | margin: 0;
30 | width: 100%;
31 | background-color: white;
32 | border: 1px solid #ccc;
33 | border-radius: 3px;
34 | overflow: hidden;
35 | }
36 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "styled-components",
4 | "transform-decorators-legacy"
5 | ],
6 | "presets": [
7 | [
8 | "env",
9 | {
10 | "modules": false
11 | }
12 | ],
13 | "react",
14 | "stage-0"
15 | ],
16 | "env": {
17 | "production": {
18 | "only": [
19 | "app"
20 | ],
21 | "plugins": [
22 | "transform-react-remove-prop-types",
23 | "transform-react-constant-elements",
24 | "transform-react-inline-elements"
25 | ]
26 | },
27 | "test": {
28 | "plugins": [
29 | "transform-es2015-modules-commonjs",
30 | "dynamic-import-node"
31 | ]
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/atoms/H1/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import H1 from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | describe(' ', () => {
10 | it('should render a prop', () => {
11 | const id = 'testId';
12 | const renderedComponent = shallow(
13 |
14 | );
15 | expect(renderedComponent.prop('id')).toEqual(id);
16 | });
17 |
18 | it('should render its text', () => {
19 | const children = 'Text';
20 | const renderedComponent = shallow(
21 | {children}
22 | );
23 | expect(renderedComponent.contains(children)).toBe(true);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/app/atoms/H2/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import H2 from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | describe(' ', () => {
10 | it('should render a prop', () => {
11 | const id = 'testId';
12 | const renderedComponent = shallow(
13 |
14 | );
15 | expect(renderedComponent.prop('id')).toEqual(id);
16 | });
17 |
18 | it('should render its text', () => {
19 | const children = 'Text';
20 | const renderedComponent = shallow(
21 | {children}
22 | );
23 | expect(renderedComponent.contains(children)).toBe(true);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/app/pages/SignOut/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { FormattedMessage } from 'react-intl';
4 | import { useHistory } from 'react-router-dom';
5 |
6 | import { signOut } from '../../store/auth/actions';
7 | import { removeToken } from '../../utils/auth';
8 |
9 | import messages from './messages';
10 |
11 | const SignOut = () => {
12 | const dispatch = useDispatch();
13 | const { push } = useHistory();
14 |
15 | useEffect(() => {
16 | dispatch(signOut());
17 | removeToken();
18 | push('/');
19 | }, []);
20 |
21 | return (
22 | ...
23 | );
24 | };
25 |
26 | export default SignOut;
27 |
--------------------------------------------------------------------------------
/app/atoms/List/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import List from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | describe('
', () => {
10 | let component;
11 | const className = 'list';
12 | const items = [
13 | 'list item',
14 | ];
15 |
16 | beforeEach(() => {
17 | component = shallow(
);
18 | });
19 |
20 | it('renders an tag', () => expect(component.type()).toBe('ul'));
21 | it('sets the className', () => expect(component.prop('className')).toBe(className));
22 | it('renders the list item', () => expect(component.find('li').first().text()).toBe(items[0]));
23 | });
24 |
--------------------------------------------------------------------------------
/app/molecules/Dropdown/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Option from './Option';
5 |
6 | function Dropdown(props) {
7 | let content = (-- );
8 |
9 | // If we have items, render them
10 | if (props.values) {
11 | content = props.values.map((value) => (
12 |
13 | ));
14 | }
15 |
16 | return (
17 |
18 | {content}
19 |
20 | );
21 | }
22 |
23 | Dropdown.propTypes = {
24 | onChange: PropTypes.func,
25 | values: PropTypes.array,
26 | value: PropTypes.string,
27 | messages: PropTypes.object,
28 | };
29 |
30 | export default Dropdown;
31 |
--------------------------------------------------------------------------------
/app/utils/auth/index.js:
--------------------------------------------------------------------------------
1 | const TOKEN_FIELD_NAME = 'jwt_token';
2 |
3 | /**
4 | * Save a token string in Local Storage
5 | *
6 | * @param {string} token
7 | */
8 | export function setToken(token) {
9 | localStorage.setItem(TOKEN_FIELD_NAME, token);
10 | }
11 |
12 | /**
13 | * Remove a token from Local Storage
14 | *
15 | */
16 | export function removeToken() {
17 | localStorage.removeItem(TOKEN_FIELD_NAME);
18 | }
19 |
20 | /**
21 | * Get a token value
22 | *
23 | * @returns {string}
24 | */
25 | export function getToken() {
26 | return localStorage.getItem(TOKEN_FIELD_NAME);
27 | }
28 |
29 | /**
30 | * Check if a token is saved in Local Storage
31 | *
32 | * @returns {boolean}
33 | */
34 | export function isTokenSet() {
35 | return getToken() !== null;
36 | }
37 |
--------------------------------------------------------------------------------
/server/middlewares/addProdMiddlewares.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const compression = require('compression');
4 |
5 | module.exports = function addProdMiddlewares(app, options) {
6 | const publicPath = options.publicPath || '/';
7 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build');
8 |
9 | // compression middleware compresses your server responses which makes them
10 | // smaller (applies also to assets). You can read more about that technique
11 | // and other good practices on official Express.js docs http://mxs.is/googmy
12 | app.use(compression());
13 | app.use(publicPath, express.static(outputPath));
14 |
15 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));
16 | };
17 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/TitleIntl/Title/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import { Helmet } from 'react-helmet';
6 |
7 | import Title from './';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
11 | describe(' ', () => {
12 | let component;
13 | const content = 'The title';
14 |
15 | beforeEach(() => {
16 | component = shallow( );
17 | });
18 |
19 | it('renders a Helmet component', () => expect(component.type()).toEqual(Helmet));
20 |
21 | it('renders a title tag', () => {
22 | const title = component.childAt(0);
23 | expect(title.type()).toBe('title');
24 | expect(title.childAt(0).text()).toBe(content);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/Repository/IssueIcon/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import IssueIcon from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | describe(' ', () => {
10 | const className = 'issue-icon';
11 | let component;
12 |
13 | beforeEach(() => {
14 | component = shallow( );
15 | });
16 |
17 | it('renders a SVG', () => expect(component.type()).toBe('svg'));
18 | it('sets the width', () => expect(component.prop('width')).toBe('0.875em'));
19 | it('sets the height', () => expect(component.prop('height')).toBe('1em'));
20 | it('sets the height', () => expect(component.prop('className')).toBe(className));
21 | });
22 |
--------------------------------------------------------------------------------
/app/pages/404-NotFound/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Testing the NotFoundPage
3 | */
4 |
5 | import React from 'react';
6 | import Enzyme, { shallow } from 'enzyme';
7 | import { FormattedMessage } from 'react-intl';
8 | import Adapter from 'enzyme-adapter-react-16';
9 |
10 | import H1 from '../../atoms/H1';
11 | import NotFound from './';
12 |
13 | Enzyme.configure({ adapter: new Adapter() });
14 |
15 | describe(' ', () => {
16 | it('should render the Page Not Found text', () => {
17 | const renderedComponent = shallow(
18 |
19 | );
20 | expect(renderedComponent.contains(
21 |
22 |
26 | )).toEqual(true);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/app/organisms/App/AuthorizedRoute/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Redirect, Route } from 'react-router-dom';
4 |
5 | import setAuthenticatedProp from '../../../utils/authenticated';
6 |
7 | const AuthorizedRoute = ({ authenticated, component: Component, ...rest }) => (
8 | authenticated ?
11 |
12 | :
13 |
14 | }
15 | />
16 | );
17 |
18 | AuthorizedRoute.propTypes = {
19 | authenticated: PropTypes.bool,
20 | component: PropTypes.oneOfType([
21 | PropTypes.node,
22 | PropTypes.func,
23 | ]).isRequired,
24 | };
25 |
26 | export default setAuthenticatedProp(AuthorizedRoute);
27 |
--------------------------------------------------------------------------------
/app/organisms/App/UnauthorizedRoute/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Redirect, Route } from 'react-router-dom';
4 |
5 | import setAuthenticatedProp from '../../../utils/authenticated';
6 |
7 | const UnauthorizedRoute = ({ authenticated, component: Component, ...rest }) => (
8 | authenticated ?
11 |
12 | :
13 |
14 | }
15 | />
16 | );
17 |
18 | UnauthorizedRoute.propTypes = {
19 | authenticated: PropTypes.bool,
20 | component: PropTypes.oneOfType([
21 | PropTypes.node,
22 | PropTypes.func,
23 | ]).isRequired,
24 | };
25 |
26 | export default setAuthenticatedProp(UnauthorizedRoute);
27 |
--------------------------------------------------------------------------------
/app/utils/form/index.js:
--------------------------------------------------------------------------------
1 | export const FORM_SUBMISSION_FAILED = 'FORM_SUBMISSION_FAILED';
2 |
3 | export default async function (submitRequest, setData, intl, messages, { resetForm, setErrors, setSubmitting }) {
4 | setErrors({});
5 | setSubmitting(true);
6 |
7 | try {
8 | const authData = await submitRequest();
9 | setSubmitting(false);
10 |
11 | if (authData.errors === undefined) {
12 | resetForm({});
13 | setData(authData.payload);
14 | } else {
15 | setErrors(Object.keys(authData.errors).reduce((errors, field) => Object.assign(errors, {
16 | [field === '' ? '_error' : field]: intl.formatMessage(messages[authData.errors[field].code]),
17 | }), {}));
18 | }
19 | } catch (err) {
20 | setErrors({ _error: intl.formatMessage(messages[FORM_SUBMISSION_FAILED]) });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/Feature/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import { IntlProvider } from 'react-intl';
5 |
6 | import Feature from './';
7 |
8 | const description = {
9 | id: 'boilerplate.pages.FeatureList.network.message',
10 | defaultMessage: `
11 | The next frontier in performant web apps: availability without a
12 | network connection from the instant your users load the app.
13 | `,
14 | };
15 | const header = {
16 | id: 'boilerplate.pages.FeatureList.network.header',
17 | defaultMessage: 'Offline-first',
18 | };
19 |
20 | storiesOf('Feature', module)
21 | .addDecorator((story) => (
22 |
23 | {story()}
24 |
25 | ))
26 | .add('default', () => )
27 | ;
28 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/MetaIntl/Meta/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import { Helmet } from 'react-helmet';
6 |
7 | import Meta from './';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
11 | describe(' ', () => {
12 | let component;
13 | const content = 'The description';
14 | const type = 'description';
15 |
16 | beforeEach(() => {
17 | component = shallow( );
18 | });
19 |
20 | it('renders a Helmet component', () => expect(component.type()).toEqual(Helmet));
21 |
22 | it('renders a meta tag', () => {
23 | const meta = component.childAt(0);
24 | expect(meta.type()).toBe('meta');
25 | expect(meta.prop('name')).toBe(type);
26 | expect(meta.prop('content')).toBe(content);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/app/organisms/LanguageProvider/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useSelector } from 'react-redux';
4 | import { IntlProvider } from 'react-intl';
5 | import { createStructuredSelector } from 'reselect';
6 |
7 | import { makeSelectLocale } from '../../store/language/selectors';
8 |
9 | const selector = createStructuredSelector({
10 | locale: makeSelectLocale(),
11 | });
12 |
13 | const LanguageProvider = ({ children, messages }) => {
14 | const { locale } = useSelector(selector);
15 |
16 | return (
17 |
18 | {React.Children.only(children)}
19 |
20 | );
21 | };
22 |
23 | LanguageProvider.propTypes = {
24 | messages: PropTypes.object,
25 | children: PropTypes.element.isRequired,
26 | };
27 |
28 | export default LanguageProvider;
29 |
--------------------------------------------------------------------------------
/app/organisms/LanguageProvider/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { defineMessages } from 'react-intl';
5 |
6 | import LanguageProvider from './';
7 |
8 | Enzyme.configure({ adapter: new Adapter() });
9 |
10 | const messages = defineMessages({
11 | someMessage: {
12 | id: 'some.id',
13 | defaultMessage: 'This is some default message',
14 | en: 'This is some en message',
15 | },
16 | });
17 |
18 | describe(' ', () => {
19 | it('should render its children', () => {
20 | const children = (Test );
21 | const renderedComponent = shallow(
22 |
23 | {children}
24 |
25 | );
26 | expect(renderedComponent.contains(children)).toBe(true);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/app/organisms/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 | import styled from 'styled-components';
4 |
5 | import A from '../../atoms/A';
6 | import LocaleSwitcher from '../LocaleSwitcher';
7 |
8 | import messages from './messages';
9 |
10 | const Wrapper = styled.footer`
11 | display: flex;
12 | justify-content: space-between;
13 | margin-top: 2em;
14 | padding: 2em 0;
15 | `;
16 |
17 | function Footer() {
18 | return (
19 |
20 |
23 |
26 |
27 | Alex Elgin,
31 | }}
32 | />
33 |
34 |
35 | );
36 | }
37 |
38 | export default Footer;
39 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/Repository/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FormattedNumber } from 'react-intl';
4 |
5 | import IssueIcon from './IssueIcon';
6 | import IssuesLink from './IssuesLink';
7 | import RepositoryLink from './RepositoryLink';
8 | import styles from './styles.scss';
9 |
10 | export const Repository = ({ repository, username }) => (
11 |
12 |
13 | {repository.owner.login === username ? repository.name : repository.full_name}
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
22 | Repository.propTypes = {
23 | repository: PropTypes.object,
24 | username: PropTypes.string,
25 | };
26 |
27 | export default Repository;
28 |
--------------------------------------------------------------------------------
/app/pages/SignOut/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import { FormattedMessage } from 'react-intl';
6 |
7 | import SignOutPage from './';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
11 | describe(' ', () => {
12 | let component;
13 | let signOut;
14 |
15 | beforeEach(() => {
16 | signOut = jest.fn();
17 | component = shallow( );
18 | });
19 |
20 | it('renders default content', () => {
21 | expect(component.type()).toBe('p');
22 | expect(component.childAt(1).text()).toEqual('...');
23 |
24 | const message = component.childAt(0);
25 | expect(message.type()).toEqual(FormattedMessage);
26 | expect(message.prop('id')).toBe('boilerplate.pages.SignOut.signOutMessage');
27 | expect(message.prop('defaultMessage')).toBe('Signing out');
28 | });
29 |
30 | afterEach(() => expect(signOut).toBeCalled());
31 | });
32 |
--------------------------------------------------------------------------------
/app/molecules/Page/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { injectIntl, intlShape } from 'react-intl';
4 | import { useDispatch } from 'react-redux';
5 |
6 | import { MetaIntl, TitleIntl } from '../Helmet';
7 | import { setPageTitle as setPageTitleAction } from '../../store/nav/actions';
8 |
9 | const Page = ({ children, description, intl, title }) => {
10 | const dispatch = useDispatch();
11 | const setPageTitle = (pageTitle) => dispatch(setPageTitleAction(pageTitle));
12 |
13 | useEffect(() => {
14 | setPageTitle(intl.formatMessage(title));
15 | }, []);
16 |
17 | return (
18 |
19 |
20 | {description === undefined ? null : }
21 | {children}
22 |
23 | );
24 | };
25 |
26 | Page.propTypes = {
27 | children: PropTypes.node,
28 | description: PropTypes.object,
29 | intl: intlShape.isRequired,
30 | title: PropTypes.object,
31 | };
32 |
33 | export default injectIntl(Page);
34 |
--------------------------------------------------------------------------------
/app/organisms/SignInForm/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { IntlProvider } from 'react-intl';
6 | import { Provider } from 'react-redux';
7 | import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles';
8 |
9 | import SignInForm from './';
10 | import defaultTheme from '../App/defaultTheme';
11 | import configureStore from '../../utils/store';
12 | import authReducer from '../../store/auth/reducer';
13 |
14 | const store = configureStore({
15 | auth: authReducer,
16 | });
17 |
18 | storiesOf('Sign In', module)
19 | .addDecorator((story) => (
20 |
21 |
22 |
23 |
24 | {story()}
25 |
26 |
27 |
28 |
29 | ))
30 | .add('default', () =>
31 | {}} onSubmit={() => {}} />
32 | )
33 | ;
34 |
--------------------------------------------------------------------------------
/app/organisms/SignUpForm/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { IntlProvider } from 'react-intl';
6 | import { Provider } from 'react-redux';
7 | import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles';
8 |
9 | import SignUpForm from './';
10 | import defaultTheme from '../App/defaultTheme';
11 | import configureStore from '../../utils/store';
12 | import authReducer from '../../store/auth/reducer';
13 |
14 | const store = configureStore({
15 | auth: authReducer,
16 | });
17 |
18 | storiesOf('Sign Up', module)
19 | .addDecorator((story) => (
20 |
21 |
22 |
23 |
24 | {story()}
25 |
26 |
27 |
28 |
29 | ))
30 | .add('default', () =>
31 | {}} onSubmit={() => {}} />
32 | )
33 | ;
34 |
--------------------------------------------------------------------------------
/app/organisms/LocaleSwitcher/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import Enzyme, { shallow } from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-16';
5 |
6 | import LocaleSwitcher from './';
7 | import LanguageProvider from '../LanguageProvider';
8 |
9 | import reducer from '../../store/language/reducer';
10 | import configureStore from '../../utils/store';
11 | import translationMessages from '../../i18n';
12 |
13 | Enzyme.configure({ adapter: new Adapter() });
14 |
15 | describe('LocaleSwitcher component', () => {
16 | let store;
17 |
18 | beforeAll(() => {
19 | store = configureStore({
20 | language: reducer,
21 | });
22 | });
23 |
24 | it('should render the default language messages', () => {
25 | const renderedComponent = shallow(
26 |
27 |
28 |
29 |
30 |
31 | );
32 | expect(renderedComponent.contains( )).toBe(true);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/TitleIntl/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { mountWithIntl } from 'enzyme-react-intl';
5 |
6 | import { FormattedMessage } from 'react-intl';
7 |
8 | import TitleIntl from './';
9 |
10 | import Title from './Title';
11 |
12 | Enzyme.configure({ adapter: new Adapter() });
13 |
14 | describe(' ', () => {
15 | let component;
16 | const message = {
17 | id: 'boilerplate.pages.FeatureList.metaTitle',
18 | defaultMessage: 'Features List',
19 | };
20 |
21 | beforeEach(() => {
22 | component = mountWithIntl( ).find(FormattedMessage).first();
23 | });
24 |
25 | it('renders a FormattedMessage component', () => {
26 | Object.keys(message).map((field) => expect(component.prop(field)).toEqual(message[field]));
27 | });
28 |
29 | it('renders a Title component', () => {
30 | const title = component.childAt(0);
31 | expect(title.type()).toEqual(Title);
32 | expect(title.prop('content')).toBe(message.defaultMessage);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/app/organisms/LocaleSwitcher/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { createStructuredSelector } from 'reselect';
4 |
5 | import messages from './messages';
6 | import translationMessages from '../../i18n';
7 | import Dropdown from '../../molecules/Dropdown';
8 | import { changeLocale } from '../../store/language/actions';
9 | import { makeSelectLocale } from '../../store/language/selectors';
10 | import setYupLocale from '../../utils/yup';
11 |
12 | const selector = createStructuredSelector({
13 | locale: makeSelectLocale(),
14 | });
15 |
16 | const LocaleSwitcher = () => {
17 | const { locale } = useSelector(selector);
18 | const dispatch = useDispatch();
19 |
20 | return (
21 | {
26 | const { value: localeId } = target;
27 | setYupLocale(localeId);
28 | dispatch(changeLocale(localeId));
29 | }}
30 | />
31 | );
32 | };
33 |
34 | export default LocaleSwitcher;
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Alexander Elgin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/templates/default/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Helmet } from 'react-helmet';
4 | import styled from 'styled-components';
5 |
6 | import Header from '../../organisms/Header';
7 | import Footer from '../../organisms/Footer';
8 |
9 | const AppWrapper = styled.div`
10 | max-width: calc(768px + 16px * 2);
11 | margin: 0 auto;
12 | display: flex;
13 | min-height: 100%;
14 | padding: 1em 0;
15 | flex-direction: column;
16 | `;
17 |
18 | const Template = ({ component, ...rest }) => {
19 | const Content = component;
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | Template.propTypes = {
36 | component: PropTypes.oneOfType([
37 | PropTypes.node,
38 | PropTypes.func,
39 | ]).isRequired,
40 | };
41 |
42 | export default Template;
43 |
--------------------------------------------------------------------------------
/app/organisms/Footer/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | import A from '../../atoms/A';
7 | import messages from './messages';
8 | import Footer from './';
9 |
10 | Enzyme.configure({ adapter: new Adapter() });
11 |
12 | describe('', () => {
13 | it('should render the copyright notice', () => {
14 | const renderedComponent = shallow(
15 |
16 | );
17 | expect(renderedComponent.contains(
18 |
21 | )).toBe(true);
22 | });
23 |
24 | it('should render the credits', () => {
25 | const renderedComponent = shallow();
26 | expect(renderedComponent.contains(
27 |
28 | Alex Elgin,
32 | }}
33 | />
34 |
35 | )).toBe(true);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/app/organisms/Header/index.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf } from '@storybook/react';
3 |
4 | import { BrowserRouter } from 'react-router-dom';
5 | import { IntlProvider } from 'react-intl';
6 | import { Provider } from 'react-redux';
7 |
8 | import Header from './';
9 | import configureStore from '../../utils/store';
10 | import authReducer from '../../store/auth/reducer';
11 | import { setSignInData, signOut } from '../../store/auth/actions';
12 |
13 | const store = configureStore({
14 | auth: authReducer,
15 | });
16 |
17 | storiesOf('Header', module)
18 | .addDecorator((story) => (
19 |
20 |
21 |
22 | {story()}
23 |
24 |
25 |
26 | ))
27 | .add('not authenticated', () => {
28 | store.dispatch(signOut());
29 | return ();
30 | })
31 | .add('authenticated', () => {
32 | store.dispatch(setSignInData({
33 | user: { name: 'Dummy User', email: 'dummy@gmail.com' },
34 | token: 'dummy token',
35 | }));
36 | return ();
37 | })
38 | ;
39 |
--------------------------------------------------------------------------------
/app/utils/request/index.js:
--------------------------------------------------------------------------------
1 | import { submitRequest, extractJson } from './base';
2 |
3 | const getSerializedPayload = (payload) => Object.keys(payload).map((field) => `${field}=${payload[field]}`).join('&');
4 |
5 | const getRaw = (path, data, rootUrl) => (
6 | submitRequest(rootUrl + path + (Object.keys(data).length > 0 ? `?${getSerializedPayload(data)}` : ''), 'GET', true)
7 | );
8 |
9 | const postRaw = (path, payload, rootUrl) => submitRequest(rootUrl + path, 'POST', true, payload);
10 | const putRaw = (path, payload, rootUrl) => submitRequest(rootUrl + path, 'PUT', true, payload);
11 | const removeRaw = (path, payload, rootUrl) => submitRequest(rootUrl + path, 'DELETE', false, payload);
12 |
13 | export const get = (path, payload, rootUrl = APP_ENV_API_BASE_URL) => extractJson(getRaw(path, payload, rootUrl));
14 | export const post = (path, payload, rootUrl = APP_ENV_API_BASE_URL) => extractJson(postRaw(path, payload, rootUrl));
15 | export const put = (path, payload, rootUrl = APP_ENV_API_BASE_URL) => extractJson(putRaw(path, payload, rootUrl));
16 | export const remove = (path, payload, rootUrl = APP_ENV_API_BASE_URL) => extractJson(removeRaw(path, payload, rootUrl));
17 |
--------------------------------------------------------------------------------
/app/utils/request/base/index.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 |
3 | import { isTokenSet, getToken } from '../../auth';
4 |
5 | function getRequestParams(method, useJsonHeaders, data, tokenUsed) {
6 | const params = { method };
7 |
8 | if (useJsonHeaders) {
9 | params.headers = {
10 | Accept: 'application/json',
11 | 'Content-Type': 'application/json',
12 | };
13 | } else {
14 | params.headers = {};
15 | }
16 |
17 | if (tokenUsed && isTokenSet()) {
18 | params.headers.Authorization = `Bearer ${getToken()}`;
19 | }
20 |
21 | if (data !== undefined) {
22 | params.body = JSON.stringify(data);
23 | }
24 |
25 | return params;
26 | }
27 |
28 | export function submitRequest(url, method, useJsonHeaders, data) {
29 | return fetch(url, getRequestParams(method, useJsonHeaders, data, url.indexOf(APP_ENV_API_BASE_URL) === 0)).then(
30 | (res) => res,
31 | () => ({ statusText: '404 (Not Found)' })
32 | );
33 | }
34 |
35 | export function extractJson(request) {
36 | return new Promise((resolve, reject) => request.then(
37 | (response) => response.ok ? resolve(response.json()) : reject(new Error(response.statusText))
38 | ));
39 | }
40 |
--------------------------------------------------------------------------------
/server/middlewares/addDevMiddlewares.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const webpackDevMiddleware = require('webpack-dev-middleware');
4 | const webpackHotMiddleware = require('webpack-hot-middleware');
5 |
6 | function createWebpackMiddleware(compiler, publicPath) {
7 | return webpackDevMiddleware(compiler, {
8 | noInfo: true,
9 | publicPath,
10 | silent: true,
11 | stats: 'errors-only',
12 | });
13 | }
14 |
15 | module.exports = function addDevMiddlewares(app, webpackConfig) {
16 | const compiler = webpack(webpackConfig);
17 | const middleware = createWebpackMiddleware(compiler, webpackConfig.output.publicPath);
18 |
19 | app.use(middleware);
20 | app.use(webpackHotMiddleware(compiler));
21 |
22 | // Since webpackDevMiddleware uses memory-fs internally to store build
23 | // artifacts, we use it instead
24 | const fs = middleware.fileSystem;
25 |
26 | app.get('*', (req, res) => {
27 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => {
28 | if (err) {
29 | res.sendStatus(404);
30 | } else {
31 | res.send(file.toString());
32 | }
33 | });
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react boilerplate",
3 | "theme_color": "#b1624d",
4 | "background_color": "#fafafa",
5 | "display": "standalone",
6 | "Scope": "/",
7 | "start_url": "/",
8 | "icons": [
9 | {
10 | "src": "icon-72x72.png",
11 | "sizes": "72x72",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "icon-96x96.png",
16 | "sizes": "96x96",
17 | "type": "image/png"
18 | },
19 | {
20 | "src": "icon-128x128.png",
21 | "sizes": "128x128",
22 | "type": "image/png"
23 | },
24 | {
25 | "src": "icon-144x144.png",
26 | "sizes": "144x144",
27 | "type": "image/png"
28 | },
29 | {
30 | "src": "icon-152x152.png",
31 | "sizes": "152x152",
32 | "type": "image/png"
33 | },
34 | {
35 | "src": "icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image/png"
38 | },
39 | {
40 | "src": "icon-384x384.png",
41 | "sizes": "384x384",
42 | "type": "image/png"
43 | },
44 | {
45 | "src": "icon-512x512.png",
46 | "sizes": "512x512",
47 | "type": "image/png"
48 | }
49 | ],
50 | "splash_pages": null
51 | }
52 |
--------------------------------------------------------------------------------
/server/logger.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const chalk = require('chalk');
4 | const ip = require('ip');
5 |
6 | const divider = chalk.gray('\n-----------------------------------');
7 |
8 | /**
9 | * Logger middleware, you can customize it to make messages more personal
10 | */
11 | const logger = {
12 |
13 | // Called whenever there's an error on the server we want to print
14 | error: (err) => {
15 | console.error(chalk.red(err));
16 | },
17 |
18 | // Called when express.js app starts on given port w/o errors
19 | appStarted: (port, host, tunnelStarted) => {
20 | console.log(`Server started ! ${chalk.green('✓')}`);
21 |
22 | // If the tunnel started, log that and the URL it's available at
23 | if (tunnelStarted) {
24 | console.log(`Tunnel initialised ${chalk.green('✓')}`);
25 | }
26 |
27 | console.log(`
28 | ${chalk.bold('Access URLs:')}${divider}
29 | Localhost: ${chalk.magenta(`http://${host}:${port}`)}
30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) +
31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider}
32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)}
33 | `);
34 | },
35 | };
36 |
37 | module.exports = logger;
38 |
--------------------------------------------------------------------------------
/app/molecules/Helmet/MetaIntl/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { mountWithIntl } from 'enzyme-react-intl';
5 |
6 | import { FormattedMessage } from 'react-intl';
7 |
8 | import MetaIntl from './';
9 |
10 | import Meta from './Meta';
11 |
12 | Enzyme.configure({ adapter: new Adapter() });
13 |
14 | describe(' ', () => {
15 | let component;
16 | const type = 'description';
17 | const message = {
18 | id: 'boilerplate.pages.FeatureList.metaDescription',
19 | defaultMessage: 'Features list of React.js Boilerplate application',
20 | };
21 |
22 | beforeEach(() => {
23 | component = mountWithIntl( ).find(FormattedMessage).first();
24 | });
25 |
26 | it('renders a FormattedMessage component', () => {
27 | Object.keys(message).map((field) => expect(component.prop(field)).toEqual(message[field]));
28 | });
29 |
30 | it('renders a Title component', () => {
31 | const meta = component.childAt(0);
32 | expect(meta.type()).toEqual(Meta);
33 | expect(meta.prop('content')).toBe(message.defaultMessage);
34 | expect(meta.prop('name')).toBe(type);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverageFrom: [
3 | 'app/**/*.{js,jsx}',
4 | '!app/**/*.test.{js,jsx}',
5 | '!app/*/RbGenerated*/*.{js,jsx}',
6 | '!app/app.js',
7 | '!app/global-styles.js',
8 | '!app/reducers.js',
9 | '!app/env/index.js',
10 | '!app/atoms/*/index.js',
11 | '!app/molecules/*/index.js',
12 | '!app/organisms/*/index.js',
13 | '!app/pages/*/index.{js,jsx}',
14 | '!app/**/*.{story,stories}.{js,jsx}',
15 | ],
16 | coverageThreshold: {
17 | global: {
18 | statements: 98,
19 | branches: 91,
20 | functions: 98,
21 | lines: 98,
22 | },
23 | },
24 | moduleDirectories: [
25 | 'node_modules',
26 | 'app',
27 | ],
28 | moduleNameMapper: {
29 | '.*\\.(css|less|styl|scss|sass)$': 'identity-obj-proxy',
30 | '.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/internals/mocks/image.js',
31 | '^typeface-roboto$': '/internals/mocks/image.js',
32 | },
33 | setupFiles: [
34 | 'raf/polyfill',
35 | 'jest-localstorage-mock',
36 | ],
37 | setupTestFrameworkScriptFile: '/internals/testing/test-bundler.js',
38 | testRegex: '.*\\.(spec|test)\\.js$',
39 | };
40 |
--------------------------------------------------------------------------------
/app/pages/Home/RepositoriesList/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FormattedMessage } from 'react-intl';
4 | import CircularProgress from '@material-ui/core/CircularProgress';
5 |
6 | import List from '../../../atoms/List';
7 |
8 | import Repository from './Repository';
9 |
10 | import messages from './messages';
11 | import styles from './styles.scss';
12 |
13 | const RepositoriesList = ({ loading, error, repos, username }) => (loading || (error !== false) || (repos.length > 0))
14 | ? (
15 |
16 | {loading
17 | ?
18 | :
19 | }
20 |
21 | ) : null
22 | ;
23 |
24 | const getListItems = (repos, username) => (repos.length > 0)
25 | ? repos.map((repository) => )
26 | : [ ]
27 | ;
28 |
29 | RepositoriesList.propTypes = {
30 | loading: PropTypes.bool,
31 | error: PropTypes.any,
32 | repos: PropTypes.any,
33 | username: PropTypes.string,
34 | };
35 |
36 | export default RepositoriesList;
37 |
--------------------------------------------------------------------------------
/app/pages/Home/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 | metaTitle: {
10 | id: 'boilerplate.pages.Home.metaTitle',
11 | defaultMessage: 'Home',
12 | },
13 | metaDescription: {
14 | id: 'boilerplate.pages.Home.metaDescription',
15 | defaultMessage: 'The React.js Boilerplate application homepage',
16 | },
17 | showRepositories: {
18 | id: 'boilerplate.pages.Home.showRepositories',
19 | defaultMessage: 'Show Github repositories by',
20 | },
21 | startProjectHeader: {
22 | id: 'boilerplate.pages.Home.start_project.header',
23 | defaultMessage: 'Start your next react project in seconds',
24 | },
25 | startProjectMessage: {
26 | id: 'boilerplate.pages.Home.start_project.message',
27 | defaultMessage: 'A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices',
28 | },
29 | trymeHeader: {
30 | id: 'boilerplate.pages.Home.tryme.header',
31 | defaultMessage: 'Try me!',
32 | },
33 | username: {
34 | id: 'boilerplate.pages.Home.username',
35 | defaultMessage: 'username',
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/app/molecules/Dropdown/Option/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow, mount } from 'enzyme';
3 | import { IntlProvider, defineMessages } from 'react-intl';
4 | import Adapter from 'enzyme-adapter-react-16';
5 |
6 | import Option from './';
7 |
8 | Enzyme.configure({ adapter: new Adapter() });
9 |
10 | describe(' ', () => {
11 | it('should render default language messages', () => {
12 | const defaultEnMessage = 'someContent';
13 | const message = defineMessages({
14 | enMessage: {
15 | id: 'boilerplate.organisms.LocaleSwitcher.en',
16 | defaultMessage: defaultEnMessage,
17 | },
18 | });
19 | const renderedComponent = shallow(
20 |
21 |
22 |
23 | );
24 | expect(renderedComponent.contains( )).toBe(true);
25 | });
26 |
27 | it('should display `value`(two letter language code) when `message` is absent', () => {
28 | const renderedComponent = mount(
29 |
30 |
31 |
32 | );
33 | expect(renderedComponent.text()).toBe('de');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dll.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * WEBPACK DLL GENERATOR
3 | *
4 | * This profile is used to cache webpack's module
5 | * contexts for external library and framework type
6 | * dependencies which will usually not change often enough
7 | * to warrant building them from scratch every time we use
8 | * the webpack process.
9 | */
10 |
11 | const { join } = require('path');
12 | const defaults = require('lodash/defaultsDeep');
13 | const webpack = require('webpack');
14 | const pkg = require(join(process.cwd(), 'package.json'));
15 | const dllPlugin = require('../config').dllPlugin;
16 |
17 | if (!pkg.dllPlugin) { process.exit(0); }
18 |
19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults);
20 | const outputPath = join(process.cwd(), dllConfig.path);
21 |
22 | module.exports = require('./webpack.base.babel')({
23 | context: process.cwd(),
24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg),
25 | devtool: 'eval',
26 | output: {
27 | filename: '[name].dll.js',
28 | path: outputPath,
29 | library: '[name]',
30 | },
31 | plugins: [
32 | new webpack.DllPlugin({
33 | name: '[name]',
34 | path: join(outputPath, '[name].json'),
35 | }),
36 | ],
37 | performance: {
38 | hints: false,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/app/organisms/SignInForm/messages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | import {
4 | ERRORS_PREFIX,
5 | EMPTY_PASSWORD,
6 | INCORRECT_CREDENTIALS,
7 | INVALID_EMAIL,
8 | } from '../../store/auth/constants';
9 |
10 | import { FORM_SUBMISSION_FAILED } from '../../utils/form';
11 |
12 | const messagesData = {
13 | [FORM_SUBMISSION_FAILED]: { id: `boilerplate.shared.errors.${FORM_SUBMISSION_FAILED}` },
14 | noAccount: {
15 | id: 'boilerplate.organisms.SignInForm.noAccount',
16 | defaultMessage: 'Don\'t have an account?',
17 | },
18 | signIn: {
19 | id: 'boilerplate.shared.Auth.signIn',
20 | defaultMessage: 'Sign in',
21 | },
22 | signUp: {
23 | id: 'boilerplate.shared.Auth.signUp',
24 | defaultMessage: 'Sign up',
25 | },
26 | email: {
27 | id: 'boilerplate.shared.Auth.email',
28 | defaultMessage: 'Email',
29 | },
30 | password: {
31 | id: 'boilerplate.shared.Auth.password',
32 | defaultMessage: 'Password',
33 | },
34 | };
35 |
36 | [
37 | EMPTY_PASSWORD,
38 | INCORRECT_CREDENTIALS,
39 | INVALID_EMAIL,
40 | ].forEach((message) => {
41 | messagesData[message] = {
42 | id: `${ERRORS_PREFIX}.${message}`,
43 | defaultMessage: message,
44 | };
45 | });
46 |
47 | export default defineMessages(messagesData);
48 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | import H1 from '../../atoms/H1';
5 | import List from '../../atoms/List';
6 | import Page from '../../molecules/Page';
7 |
8 | import Feature from './Feature';
9 | import messages from './messages';
10 |
11 | import styles from './styles.scss';
12 |
13 | const featuresData = [
14 | {
15 | header: messages.scaffoldingHeader,
16 | description: messages.scaffoldingMessage,
17 | },
18 | {
19 | header: messages.scaffoldingHeader,
20 | description: messages.scaffoldingMessage,
21 | },
22 | {
23 | header: messages.feedbackHeader,
24 | description: messages.feedbackMessage,
25 | },
26 | {
27 | header: messages.routingHeader,
28 | description: messages.routingMessage,
29 | },
30 | {
31 | header: messages.networkHeader,
32 | description: messages.networkMessage,
33 | },
34 | {
35 | header: messages.intlHeader,
36 | description: messages.intlMessage,
37 | },
38 | ];
39 |
40 | const FeatureList = () => (
41 |
42 |
43 |
44 |
45 | )} />
46 |
47 | );
48 |
49 | export default FeatureList;
50 |
--------------------------------------------------------------------------------
/app/utils/request/index.test.js:
--------------------------------------------------------------------------------
1 | import { get, post, put, remove } from './';
2 |
3 | jest.mock('./base', () => ({
4 | extractJson: (responseData) => {
5 | const result = responseData;
6 | result.jsonExtracted = true;
7 | return result;
8 | },
9 | submitRequest: (url, method, headers, payload) => ({ url, method, headers, payload }),
10 | }));
11 |
12 | const checkRequest = (requestName, request, method, headers, path, customBaseUrl, payload, emptyPayload, finalPath) => {
13 | describe(`#${requestName}`, () => {
14 | describe('custom base URL with payload', () => {
15 | it(`extracts JSON from a response of the ${method} request`, () => {
16 | expect(request(path, payload, customBaseUrl)).toEqual({
17 | url: customBaseUrl + (finalPath === undefined ? path : finalPath),
18 | method,
19 | headers,
20 | payload: (emptyPayload ? undefined : payload),
21 | jsonExtracted: true,
22 | });
23 | });
24 | });
25 | });
26 | };
27 |
28 | const path = '/users';
29 |
30 | checkRequest('get', get, 'GET', true, path, 'https//github.com', { type: 'all' }, true, `${path}?type=all`);
31 | checkRequest('post', post, 'POST', true, '/users', 'https//linkedin.com', { gender: 'male' });
32 | checkRequest('put', put, 'PUT', true, '/users/2', 'https//facebook.com', { name: 'Alex' });
33 | checkRequest('remove', remove, 'DELETE', false, '/users/3', 'https//yahoo.com', { id: '123' });
34 |
--------------------------------------------------------------------------------
/app/utils/auth/index.test.js:
--------------------------------------------------------------------------------
1 | import { getToken, isTokenSet, removeToken, setToken } from './';
2 |
3 | const token = 'jwt token value';
4 | const TOKEN_FIELD_NAME = 'jwt_token';
5 |
6 | describe('#removeToken', () => {
7 | it('removes the token', () => {
8 | jest.spyOn(localStorage, 'removeItem');
9 | removeToken();
10 | expect(localStorage.removeItem).toBeCalledWith(TOKEN_FIELD_NAME);
11 | });
12 | });
13 |
14 | describe('#setToken', () => {
15 | it('saves the token', () => {
16 | jest.spyOn(localStorage, 'setItem');
17 | setToken(token);
18 | expect(localStorage.setItem).toBeCalledWith(TOKEN_FIELD_NAME, token);
19 | });
20 | });
21 |
22 | describe('#getToken', () => {
23 | it('gets the token', () => {
24 | jest.spyOn(localStorage, 'getItem');
25 | localStorage.getItem.mockReturnValue(token);
26 | expect(getToken()).toBe(token);
27 | expect(localStorage.getItem).toBeCalledWith(TOKEN_FIELD_NAME);
28 | });
29 | });
30 |
31 | describe('#isTokenSet', () => {
32 | beforeEach(() => jest.spyOn(localStorage, 'getItem'));
33 |
34 | describe('the token is set', () => {
35 | beforeEach(() => localStorage.getItem.mockReturnValue(token));
36 | it('return true', () => expect(isTokenSet()).toBe(true));
37 | });
38 |
39 | describe('the token is NOT set', () => {
40 | beforeEach(() => localStorage.getItem.mockReturnValue(null));
41 | it('return false', () => expect(isTokenSet()).toBe(false));
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/app/molecules/Dropdown/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import { IntlProvider, defineMessages } from 'react-intl';
4 | import Adapter from 'enzyme-adapter-react-16';
5 |
6 | import Dropdown from './';
7 |
8 | Enzyme.configure({ adapter: new Adapter() });
9 |
10 | describe(' ', () => {
11 | it('should contain default text', () => {
12 | const defaultEnMessage = 'someContent';
13 | const defaultDeMessage = 'someOtherContent';
14 | const messages = defineMessages({
15 | en: {
16 | id: 'boilerplate.organisms.LocaleSwitcher.en',
17 | defaultMessage: defaultEnMessage,
18 | },
19 | de: {
20 | id: 'boilerplate.organisms.LocaleSwitcher.en',
21 | defaultMessage: defaultDeMessage,
22 | },
23 | });
24 | const renderedComponent = shallow(
25 |
26 |
27 |
28 | );
29 | expect(renderedComponent.contains( )).toBe(true);
30 | expect(renderedComponent.find('option').length).toBe(0);
31 | });
32 | it('should not have options if props.values is not defined', () => {
33 | const renderedComponent = shallow( );
34 | expect(renderedComponent.contains(-- )).toBe(true);
35 | expect(renderedComponent.find('option').length).toBe(1);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/app/organisms/Header/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useSelector } from 'react-redux';
4 | import { createStructuredSelector } from 'reselect';
5 | import AppBar from '@material-ui/core/AppBar';
6 | import Toolbar from '@material-ui/core/Toolbar';
7 | import Typography from '@material-ui/core/Typography';
8 | import IconButton from '@material-ui/core/IconButton';
9 | import MenuIcon from '@material-ui/icons/Menu';
10 |
11 | import { makeSelectPageTitle } from '../../store/nav/selectors';
12 | import setAuthenticatedProp from '../../utils/authenticated';
13 |
14 | import LinksBlock from './LinksBlock';
15 |
16 | const selector = createStructuredSelector({
17 | pageTitle: makeSelectPageTitle(),
18 | });
19 |
20 | const Header = ({ authenticated }) => {
21 | const [isOpen, setOpen] = useState(false);
22 | const { pageTitle } = useSelector(selector);
23 |
24 | return (
25 |
26 |
27 | setOpen(true)}>
28 |
29 |
30 |
31 | { pageTitle }
32 |
33 | setOpen(false)} isOpen={isOpen} />
34 |
35 |
36 | );
37 | };
38 |
39 | Header.propTypes = {
40 | authenticated: PropTypes.bool,
41 | };
42 |
43 | export default setAuthenticatedProp(Header);
44 |
--------------------------------------------------------------------------------
/app/organisms/SignUpForm/messages.js:
--------------------------------------------------------------------------------
1 | import { defineMessages } from 'react-intl';
2 |
3 | import {
4 | ERRORS_PREFIX,
5 | DUPLICATED_EMAIL,
6 | INCORRECT_CREDENTIALS,
7 | INVALID_EMAIL,
8 | INVALID_NAME,
9 | INVALID_PASSWORD,
10 | } from '../../store/auth/constants';
11 |
12 | import { FORM_SUBMISSION_FAILED } from '../../utils/form';
13 |
14 | const messagesData = {
15 | alreadyHaveAccount: {
16 | id: 'boilerplate.organisms.SignUpForm.alreadyHaveAccount',
17 | defaultMessage: 'Already have an account?',
18 | },
19 | [FORM_SUBMISSION_FAILED]: { id: `boilerplate.shared.errors.${FORM_SUBMISSION_FAILED}` },
20 | signIn: {
21 | id: 'boilerplate.shared.Auth.signIn',
22 | defaultMessage: 'Sign in',
23 | },
24 | signUp: {
25 | id: 'boilerplate.shared.Auth.signUp',
26 | defaultMessage: 'Sign up',
27 | },
28 | email: {
29 | id: 'boilerplate.shared.Auth.email',
30 | defaultMessage: 'Email',
31 | },
32 | name: {
33 | id: 'boilerplate.shared.Auth.name',
34 | defaultMessage: 'Name',
35 | },
36 | password: {
37 | id: 'boilerplate.shared.Auth.password',
38 | defaultMessage: 'Password',
39 | },
40 | };
41 |
42 | [
43 | DUPLICATED_EMAIL,
44 | INCORRECT_CREDENTIALS,
45 | INVALID_EMAIL,
46 | INVALID_NAME,
47 | INVALID_PASSWORD,
48 | ].forEach((message) => {
49 | messagesData[message] = {
50 | id: `${ERRORS_PREFIX}.${message}`,
51 | defaultMessage: message,
52 | };
53 | });
54 |
55 | export default defineMessages(messagesData);
56 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint consistent-return:0 */
2 |
3 | const express = require('express');
4 | const logger = require('./logger');
5 |
6 | const argv = require('./argv');
7 | const port = require('./port');
8 | const setup = require('./middlewares/frontendMiddleware');
9 | const isDev = process.env.NODE_ENV !== 'production';
10 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false;
11 | const resolve = require('path').resolve;
12 | const app = express();
13 |
14 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here
15 | // app.use('/api', myApi);
16 |
17 | // In production we need to pass these values in instead of relying on webpack
18 | setup(app, {
19 | outputPath: resolve(process.cwd(), 'build'),
20 | publicPath: '/',
21 | });
22 |
23 | // get the intended host and port number, use localhost and port 3000 if not provided
24 | const customHost = argv.host || process.env.HOST;
25 | const host = customHost || null; // Let http.Server use its default IPv6/4 host
26 | const prettyHost = customHost || 'localhost';
27 |
28 | // Start your app.
29 | app.listen(port, host, (err) => {
30 | if (err) {
31 | return logger.error(err.message);
32 | }
33 |
34 | // Connect to ngrok in dev mode
35 | if (ngrok) {
36 | ngrok.connect(port, (innerErr, url) => {
37 | if (innerErr) {
38 | return logger.error(innerErr);
39 | }
40 |
41 | logger.appStarted(port, prettyHost, url);
42 | });
43 | } else {
44 | logger.appStarted(port, prettyHost);
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import { FormattedMessage } from 'react-intl';
4 | import Adapter from 'enzyme-adapter-react-16';
5 |
6 | import H1 from '../../atoms/H1';
7 | import { MetaIntl, TitleIntl } from '../../molecules/Helmet';
8 |
9 | import FeaturePage from './';
10 | import messages from './messages';
11 |
12 | Enzyme.configure({ adapter: new Adapter() });
13 |
14 | describe(' ', () => {
15 | let component;
16 |
17 | beforeEach(() => {
18 | component = shallow(
19 |
20 | );
21 | });
22 |
23 | it('renders the header', () => {
24 | expect(component.contains(
25 |
26 |
27 |
28 | )).toBe(true);
29 | });
30 |
31 | it('never re-renders', () => expect(component.instance().shouldComponentUpdate()).toBe(false));
32 |
33 | it('sets the page title', () => {
34 | const title = component.find(TitleIntl).first();
35 | expect(title.prop('id')).toBe('boilerplate.pages.FeatureList.metaTitle');
36 | expect(title.prop('defaultMessage')).toBe('Features List');
37 | });
38 |
39 | it('sets the page description', () => {
40 | const description = component.find(MetaIntl).first();
41 | expect(description.prop('name')).toBe('description');
42 | expect(description.prop('id')).toBe('boilerplate.pages.FeatureList.metaDescription');
43 | expect(description.prop('defaultMessage')).toBe('Features list of React.js Boilerplate application');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/internals/scripts/dependencies.js:
--------------------------------------------------------------------------------
1 | // No need to build the DLL in production
2 | if (process.env.NODE_ENV === 'production') {
3 | process.exit(0);
4 | }
5 |
6 | require('shelljs/global');
7 |
8 | const path = require('path');
9 | const fs = require('fs');
10 | const exists = fs.existsSync;
11 | const writeFile = fs.writeFileSync;
12 |
13 | const defaults = require('lodash/defaultsDeep');
14 | const pkg = require(path.join(process.cwd(), 'package.json'));
15 | const config = require('../config');
16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults);
17 | const outputPath = path.join(process.cwd(), dllConfig.path);
18 | const dllManifestPath = path.join(outputPath, 'package.json');
19 |
20 | /**
21 | * I use node_modules/react-boilerplate-dlls by default just because
22 | * it isn't going to be version controlled and babel wont try to parse it.
23 | */
24 | mkdir('-p', outputPath);
25 |
26 | echo('Building the Webpack DLL...');
27 |
28 | /**
29 | * Create a manifest so npm install doesn't warn us
30 | */
31 | if (!exists(dllManifestPath)) {
32 | writeFile(
33 | dllManifestPath,
34 | JSON.stringify(defaults({
35 | name: 'react-boilerplate-dlls',
36 | private: true,
37 | author: pkg.author,
38 | repository: pkg.repository,
39 | version: pkg.version,
40 | }), null, 2),
41 | 'utf8'
42 | );
43 | }
44 |
45 | // the BUILDING_DLL env var is set to avoid confusing the development environment
46 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js --hide-modules');
47 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/Feature/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { FormattedMessage } from 'react-intl';
5 |
6 | import Feature from './';
7 |
8 | Enzyme.configure({ adapter: new Adapter() });
9 |
10 | describe(' ', () => {
11 | const descriptionData = {
12 | id: 'boilerplate.pages.FeatureList.network.message',
13 | defaultMessage: `
14 | The next frontier in performant web apps: availability without a
15 | network connection from the instant your users load the app.
16 | `,
17 | };
18 | const headerData = {
19 | id: 'boilerplate.pages.FeatureList.network.header',
20 | defaultMessage: 'Offline-first',
21 | };
22 |
23 | let component;
24 |
25 | beforeEach(() => {
26 | component = shallow( );
27 | });
28 |
29 | it('renders the description', () => {
30 | const description = component.find('.feature-description').first().childAt(0);
31 | expect(description.type()).toEqual(FormattedMessage);
32 | expect(description.prop('id')).toBe(descriptionData.id);
33 | expect(description.prop('defaultMessage')).toBe(descriptionData.defaultMessage);
34 | });
35 |
36 | it('renders the header', () => {
37 | const header = component.find('.feature-header').first().childAt(0);
38 | expect(header.type()).toEqual(FormattedMessage);
39 | expect(header.prop('id')).toBe(headerData.id);
40 | expect(header.prop('defaultMessage')).toBe(headerData.defaultMessage);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/app/atoms/Img/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import Img from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | const src = 'test.png';
10 | const alt = 'test';
11 | const renderComponent = (props = {}) => shallow(
12 |
13 | );
14 |
15 | describe(' ', () => {
16 | it('should render an tag', () => {
17 | const renderedComponent = renderComponent();
18 | expect(renderedComponent.is('img')).toBe(true);
19 | });
20 |
21 | it('should have an src attribute', () => {
22 | const renderedComponent = renderComponent();
23 | expect(renderedComponent.prop('src')).toEqual(src);
24 | });
25 |
26 | it('should have an alt attribute', () => {
27 | const renderedComponent = renderComponent();
28 | expect(renderedComponent.prop('alt')).toEqual(alt);
29 | });
30 |
31 | it('should not have a className attribute', () => {
32 | const renderedComponent = renderComponent();
33 | expect(renderedComponent.prop('className')).toBeUndefined();
34 | });
35 |
36 | it('should adopt a className attribute', () => {
37 | const className = 'test';
38 | const renderedComponent = renderComponent({ className });
39 | expect(renderedComponent.hasClass(className)).toBe(true);
40 | });
41 |
42 | it('should not adopt a srcset attribute', () => {
43 | const srcset = 'test-HD.png 2x';
44 | const renderedComponent = renderComponent({ srcset });
45 | expect(renderedComponent.prop('srcset')).toBeUndefined();
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/app/organisms/App/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Switch } from 'react-router-dom';
3 | import 'typeface-roboto';
4 | import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles';
5 |
6 | import DefaultTemplate from '../../templates/default';
7 | import defaultTheme from './defaultTheme';
8 |
9 | import NotFoundPage from '../../pages/404-NotFound';
10 | import HomePage from '../../pages/Home';
11 | import FeatureListPage from '../../pages/FeatureList';
12 |
13 | import SignInPage from '../../pages/SignIn';
14 | import SignOutPage from '../../pages/SignOut';
15 | import SignUpPage from '../../pages/SignUp';
16 |
17 | import AuthorizedRoute from './AuthorizedRoute';
18 | import UnauthorizedRoute from './UnauthorizedRoute';
19 | import GlobalStyles from './global-styles';
20 |
21 | const App = () => (
22 |
23 |
24 | } />
25 | } />
26 | } />
27 | } />
28 | } />
29 | } />
30 |
31 |
32 |
33 | );
34 |
35 | export default App;
36 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | React.js Boilerplate
23 |
24 |
25 |
26 | If you're seeing this message, that means JavaScript has been disabled on your browser , please enable JS to make this app work.
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/organisms/App/AuthorizedRoute/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import AuthorizedRoute from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | const routerMock = require('react-router-dom');
10 | routerMock.Redirect = (props) => ;
11 | routerMock.Route = (props) => ;
12 |
13 | describe(' ', () => {
14 | let component;
15 | let wrapper;
16 | const dummyName = 'dummy-name';
17 | const Dummy = (props) => (Dummy content );
18 |
19 | describe('authenticated', () => {
20 | const dummyClass = 'dummy-class';
21 |
22 | beforeEach(() => {
23 | wrapper = shallow( );
24 | component = shallow(wrapper.prop('render')({ className: dummyClass }));
25 | });
26 |
27 | it('renders the component', () => {
28 | expect(component.type()).toBe('dummy');
29 | expect(component.prop('name')).toBe(dummyName);
30 | expect(component.prop('className')).toBe(dummyClass);
31 | });
32 | });
33 |
34 | describe('NOT authenticated', () => {
35 | const location = 'current location';
36 |
37 | beforeEach(() => {
38 | wrapper = shallow( );
39 | component = shallow(wrapper.prop('render')({ location }));
40 | });
41 |
42 | it('redirects to the home page', () => {
43 | expect(component.type()).toBe('redirect');
44 | expect(component.prop('to')).toEqual({
45 | pathname: '/',
46 | state: { from: location },
47 | });
48 | });
49 | });
50 |
51 | afterEach(() => {
52 | expect(wrapper.type()).toEqual(routerMock.Route);
53 | expect(wrapper.prop('name')).toBe(dummyName);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/app/organisms/App/UnauthorizedRoute/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | import UnauthorizedRoute from './';
6 |
7 | Enzyme.configure({ adapter: new Adapter() });
8 |
9 | const routerMock = require('react-router-dom');
10 | routerMock.Redirect = (props) => ;
11 | routerMock.Route = (props) => ;
12 |
13 | describe(' ', () => {
14 | let component;
15 | let wrapper;
16 | const dummyName = 'dummy-name';
17 | const Dummy = (props) => (Dummy content );
18 |
19 | describe('NOT authenticated', () => {
20 | const dummyClass = 'dummy-class';
21 |
22 | beforeEach(() => {
23 | wrapper = shallow( );
24 | component = shallow(wrapper.prop('render')({ className: dummyClass }));
25 | });
26 |
27 | it('renders the component', () => {
28 | expect(component.type()).toBe('dummy');
29 | expect(component.prop('name')).toBe(dummyName);
30 | expect(component.prop('className')).toBe(dummyClass);
31 | });
32 | });
33 |
34 | describe('authenticated', () => {
35 | const location = 'current location';
36 |
37 | beforeEach(() => {
38 | wrapper = shallow( );
39 | component = shallow(wrapper.prop('render')({ location }));
40 | });
41 |
42 | it('redirects to the home page', () => {
43 | expect(component.type()).toBe('redirect');
44 | expect(component.prop('to')).toEqual({
45 | pathname: '/',
46 | state: { from: location },
47 | });
48 | });
49 | });
50 |
51 | afterEach(() => {
52 | expect(wrapper.type()).toEqual(routerMock.Route);
53 | expect(wrapper.prop('name')).toBe(dummyName);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/app/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #######################################################################
5 | # GENERAL #
6 | #######################################################################
7 |
8 | # Make apache follow sym links to files
9 | Options +FollowSymLinks
10 | # If somebody opens a folder, hide all files from the resulting folder list
11 | IndexIgnore */*
12 |
13 |
14 | #######################################################################
15 | # REWRITING #
16 | #######################################################################
17 |
18 | # Enable rewriting
19 | RewriteEngine On
20 |
21 | # If its not HTTPS
22 | RewriteCond %{HTTPS} off
23 |
24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL
25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https
26 |
27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect
28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L]
29 |
30 | # If we get to here, it means we are on https://
31 |
32 | # If the file with the specified name in the browser doesn't exist
33 | RewriteCond %{REQUEST_FILENAME} !-f
34 |
35 | # and the directory with the specified name in the browser doesn't exist
36 | RewriteCond %{REQUEST_FILENAME} !-d
37 |
38 | # and we are not opening the root already (otherwise we get a redirect loop)
39 | RewriteCond %{REQUEST_FILENAME} !\/$
40 |
41 | # Rewrite all requests to the root
42 | RewriteRule ^(.*) /
43 |
44 |
45 |
46 |
47 | # Do not cache sw.js, required for offline-first updates.
48 |
49 | Header set Cache-Control "private, no-cache, no-store, proxy-revalidate, no-transform"
50 | Header set Pragma "no-cache"
51 |
52 |
53 |
--------------------------------------------------------------------------------
/internals/config.js:
--------------------------------------------------------------------------------
1 | const resolve = require('path').resolve;
2 | const pullAll = require('lodash/pullAll');
3 | const uniq = require('lodash/uniq');
4 |
5 | const ReactBoilerplate = {
6 | // This refers to the react-boilerplate version this project is based on.
7 | version: '1.0.0',
8 |
9 | /**
10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading
11 | * by caching the module metadata for all of our npm dependencies. We enable it by default
12 | * in development.
13 | *
14 | *
15 | * To disable the DLL Plugin, set this value to false.
16 | */
17 | dllPlugin: {
18 | defaults: {
19 | /**
20 | * we need to exclude dependencies which are not intended for the browser
21 | * by listing them here.
22 | */
23 | exclude: [
24 | 'chalk',
25 | 'compression',
26 | 'cross-env',
27 | 'express',
28 | 'ip',
29 | 'minimist',
30 | 'sanitize.css',
31 | 'react-icons',
32 | ],
33 |
34 | /**
35 | * Specify any additional dependencies here. We include core-js and lodash
36 | * since a lot of our dependencies depend on them and they get picked up by webpack.
37 | */
38 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'],
39 |
40 | // The path where the DLL manifest and bundle will get built
41 | path: resolve('../node_modules/react-boilerplate-dlls'),
42 | },
43 |
44 | entry(pkg) {
45 | const dependencyNames = Object.keys(pkg.dependencies);
46 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude;
47 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include;
48 | const includeDependencies = uniq(dependencyNames.concat(include));
49 |
50 | return {
51 | reactBoilerplateDeps: pullAll(includeDependencies, exclude),
52 | };
53 | },
54 | },
55 | };
56 |
57 | module.exports = ReactBoilerplate;
58 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
2 |
3 | # Handle line endings automatically for files detected as text
4 | # and leave all files detected as binary untouched.
5 | * text=auto
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | #
12 | ## These files are text and should be normalized (Convert crlf => lf)
13 | #
14 |
15 | # source code
16 | *.php text
17 | *.css text
18 | *.sass text
19 | *.scss text
20 | *.less text
21 | *.styl text
22 | *.js text eol=lf
23 | *.coffee text
24 | *.json text
25 | *.htm text
26 | *.html text
27 | *.xml text
28 | *.svg text
29 | *.txt text
30 | *.ini text
31 | *.inc text
32 | *.pl text
33 | *.rb text
34 | *.py text
35 | *.scm text
36 | *.sql text
37 | *.sh text
38 | *.bat text
39 |
40 | # templates
41 | *.ejs text
42 | *.hbt text
43 | *.jade text
44 | *.haml text
45 | *.hbs text
46 | *.dot text
47 | *.tmpl text
48 | *.phtml text
49 |
50 | # server config
51 | .htaccess text
52 | .nginx.conf text
53 |
54 | # git config
55 | .gitattributes text
56 | .gitignore text
57 | .gitconfig text
58 |
59 | # code analysis config
60 | .jshintrc text
61 | .jscsrc text
62 | .jshintignore text
63 | .csslintrc text
64 |
65 | # misc config
66 | *.yaml text
67 | *.yml text
68 | .editorconfig text
69 |
70 | # build config
71 | *.npmignore text
72 | *.bowerrc text
73 |
74 | # Heroku
75 | Procfile text
76 | .slugignore text
77 |
78 | # Documentation
79 | *.md text
80 | LICENSE text
81 | AUTHORS text
82 |
83 |
84 | #
85 | ## These files are binary and should be left untouched
86 | #
87 |
88 | # (binary is a macro for -text -diff)
89 | *.png binary
90 | *.jpg binary
91 | *.jpeg binary
92 | *.gif binary
93 | *.ico binary
94 | *.mov binary
95 | *.mp4 binary
96 | *.mp3 binary
97 | *.flv binary
98 | *.fla binary
99 | *.swf binary
100 | *.gz binary
101 | *.zip binary
102 | *.7z binary
103 | *.ttf binary
104 | *.eot binary
105 | *.woff binary
106 | *.pyc binary
107 | *.pdf binary
108 |
--------------------------------------------------------------------------------
/app/organisms/Header/LinksBlock/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { FormattedMessage } from 'react-intl';
4 | import { Link, withRouter } from 'react-router-dom';
5 | import {
6 | Drawer,
7 | List,
8 | ListItem,
9 | ListItemIcon,
10 | ListItemText,
11 | } from '@material-ui/core';
12 | import {
13 | DeveloperBoard,
14 | ExitToApp,
15 | Home,
16 | PersonAdd,
17 | PowerSettingsNew,
18 | } from '@material-ui/icons';
19 |
20 | import messages from './messages';
21 |
22 | const getLinksData = (authenticated) => {
23 | const linksData = [
24 | {
25 | icon: Home,
26 | text: messages.home,
27 | uri: '/',
28 | },
29 | {
30 | icon: DeveloperBoard,
31 | text: messages.features,
32 | uri: '/features',
33 | },
34 | ];
35 |
36 | if (authenticated) {
37 | linksData.push({
38 | icon: PowerSettingsNew,
39 | text: messages.signOut,
40 | uri: '/signOut',
41 | });
42 | } else {
43 | [
44 | {
45 | icon: ExitToApp,
46 | text: messages.signIn,
47 | uri: '/signin',
48 | },
49 | {
50 | icon: PersonAdd,
51 | text: messages.signUp,
52 | uri: '/signup',
53 | },
54 | ].forEach((linkData) => linksData.push(linkData));
55 | }
56 |
57 | return linksData;
58 | };
59 |
60 | const LinksBlock = ({ authenticated, close, isOpen, location }) => (
61 |
62 |
63 |
64 | {getLinksData(authenticated).map(({ icon: Icon, text, uri }) => (
65 |
66 |
67 |
68 |
69 | ))}
70 |
71 |
72 |
73 | );
74 |
75 | LinksBlock.propTypes = {
76 | authenticated: PropTypes.bool,
77 | close: PropTypes.func.isRequired,
78 | isOpen: PropTypes.bool,
79 | location: PropTypes.object.isRequired,
80 | };
81 |
82 | export default withRouter(LinksBlock);
83 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "jest": true,
8 | "es6": true
9 | },
10 | "globals": {
11 | "APP_ENV_API_BASE_URL": false,
12 | "APP_ENV_BASE_PATH": false
13 | },
14 | "plugins": [
15 | "react",
16 | "jsx-a11y"
17 | ],
18 | "parserOptions": {
19 | "ecmaVersion": 6,
20 | "sourceType": "module",
21 | "ecmaFeatures": {
22 | "jsx": true
23 | }
24 | },
25 | "rules": {
26 | "arrow-parens": [
27 | "error",
28 | "always"
29 | ],
30 | "arrow-body-style": [
31 | 2,
32 | "as-needed"
33 | ],
34 | "class-methods-use-this": 0,
35 | "comma-dangle": [
36 | 2,
37 | "always-multiline"
38 | ],
39 | "import/imports-first": 0,
40 | "import/newline-after-import": 0,
41 | "import/no-dynamic-require": 0,
42 | "import/no-extraneous-dependencies": 0,
43 | "import/no-named-as-default": 0,
44 | "import/no-unresolved": 2,
45 | "import/no-webpack-loader-syntax": 0,
46 | "import/prefer-default-export": 0,
47 | "indent": [
48 | 2,
49 | 2,
50 | {
51 | "SwitchCase": 1
52 | }
53 | ],
54 | "jsx-a11y/aria-props": 2,
55 | "jsx-a11y/heading-has-content": 0,
56 | "jsx-a11y/href-no-hash": 2,
57 | "jsx-a11y/label-has-for": 2,
58 | "jsx-a11y/mouse-events-have-key-events": 2,
59 | "jsx-a11y/role-has-required-aria-props": 2,
60 | "jsx-a11y/role-supports-aria-props": 2,
61 | "max-len": 0,
62 | "newline-per-chained-call": 0,
63 | "no-confusing-arrow": 0,
64 | "no-console": 1,
65 | "no-use-before-define": 0,
66 | "prefer-template": 2,
67 | "react/forbid-prop-types": 0,
68 | "react/jsx-first-prop-new-line": [
69 | 2,
70 | "multiline"
71 | ],
72 | "react/jsx-filename-extension": 0,
73 | "react/jsx-no-target-blank": 0,
74 | "react/require-default-props": 0,
75 | "react/require-extension": 0,
76 | "react/self-closing-comp": 0,
77 | "require-yield": 0
78 | },
79 | "settings": {
80 | "import/resolver": {
81 | "webpack": {
82 | "config": "./internals/webpack/webpack.prod.babel.js"
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Atomic React Redux
2 |
3 |
4 |
5 |
6 |
7 | A React & Redux front-end boilerplate based on
8 | [atomic design][atomic-design-url] methodology
9 |
10 | [![node][node]][package-url]
11 | [![Dependencies][deps]][deps-url]
12 | [![Build Status][build-status]][build-status-url]
13 | [![Coverage][cover]][cover-url]
14 | [![Code Quality][code-quality]][code-quality-url]
15 |
16 | ## Main Features
17 | - ES6
18 | - eslint
19 | - i18N translation
20 | - jest
21 | - material-ui
22 | - postcss
23 | - react-icons
24 | - redux-form
25 | - redux-saga
26 | - reselect
27 | - storybook
28 | - styled-components
29 | - token-based authentication
30 | - webpack
31 |
32 | ## Dependencies
33 | [![Package Info][package-info]][package-url]
34 |
35 | For auth features (back end) you can use
36 | [express-passport-boilerplate][backend-url]
37 |
38 | ## Install
39 | ```sh
40 | $ yarn install
41 | ```
42 |
43 | ## Launch
44 | ```sh
45 | $ yarn start
46 | ```
47 |
48 | ## Tests
49 | ```sh
50 | $ yarn test
51 | ```
52 |
53 | ## License
54 |
55 | [![MIT][license]][license-url] Inspired by
56 | [react-boilerplate](https://github.com/react-boilerplate/react-boilerplate)
57 |
58 | [atomic-design-url]: http://bradfrost.com/blog/post/atomic-web-design/
59 | [backend-url]: https://github.com/alexander-elgin/express-passport-boilerplate
60 | [package-url]: https://npmjs.com/package/atomic-react-redux
61 |
62 | [package-info]: https://nodei.co/npm/atomic-react-redux.png
63 |
64 | [node]: https://img.shields.io/node/v/atomic-react-redux.svg
65 |
66 | [deps]: https://david-dm.org/alexander-elgin/atomic-react-redux.svg
67 | [deps-url]: https://david-dm.org/alexander-elgin/atomic-react-redux
68 |
69 | [build-status]: https://img.shields.io/travis/alexander-elgin/atomic-react-redux.svg
70 | [build-status-url]: https://travis-ci.org/alexander-elgin/atomic-react-redux
71 |
72 | [code-quality]: https://api.codacy.com/project/badge/Grade/27011bc53e004590921e1839a0b2707d
73 | [code-quality-url]: https://www.codacy.com/app/alexander-elgin/atomic-react-redux
74 |
75 | [cover]: https://coveralls.io/repos/github/alexander-elgin/atomic-react-redux/badge.svg
76 | [cover-url]: https://coveralls.io/github/alexander-elgin/atomic-react-redux
77 |
78 | [license]: https://img.shields.io/github/license/alexander-elgin/atomic-react-redux.svg
79 | [license-url]: ./LICENSE
80 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.prod.babel.js:
--------------------------------------------------------------------------------
1 | // Important modules this config uses
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const OfflinePlugin = require('offline-plugin');
6 |
7 | module.exports = require('./webpack.base.babel')({
8 | // In production, we skip all hot-reloading stuff
9 | entry: [
10 | path.join(process.cwd(), 'app/app.js'),
11 | ],
12 |
13 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets
14 | output: {
15 | filename: '[name].[chunkhash].js',
16 | chunkFilename: '[name].[chunkhash].chunk.js',
17 | },
18 |
19 | plugins: [
20 | new webpack.optimize.ModuleConcatenationPlugin(),
21 | new webpack.optimize.CommonsChunkPlugin({
22 | name: 'vendor',
23 | children: true,
24 | minChunks: 2,
25 | async: true,
26 | }),
27 |
28 | // Minify and optimize the index.html
29 | new HtmlWebpackPlugin({
30 | template: 'app/index.html',
31 | minify: {
32 | removeComments: true,
33 | collapseWhitespace: true,
34 | removeRedundantAttributes: true,
35 | useShortDoctype: true,
36 | removeEmptyAttributes: true,
37 | removeStyleLinkTypeAttributes: true,
38 | keepClosingSlash: true,
39 | minifyJS: true,
40 | minifyCSS: true,
41 | minifyURLs: true,
42 | },
43 | inject: true,
44 | }),
45 |
46 | // Put it in the end to capture all the HtmlWebpackPlugin's
47 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin
48 | new OfflinePlugin({
49 | relativePaths: false,
50 | publicPath: '/',
51 |
52 | // No need to cache .htaccess. See http://mxs.is/googmp,
53 | // this is applied before any match in `caches` section
54 | excludes: ['.htaccess'],
55 |
56 | caches: {
57 | main: [':rest:'],
58 |
59 | // All chunks marked as `additional`, loaded after main section
60 | // and do not prevent SW to install. Change to `optional` if
61 | // do not want them to be preloaded at all (cached only when first loaded)
62 | additional: ['*.chunk.js'],
63 | },
64 |
65 | // Removes warning for about `additional` section usage
66 | safeToUseOptionalCaches: true,
67 |
68 | AppCache: false,
69 | }),
70 | ],
71 |
72 | performance: {
73 | assetFilter: (assetFilename) => !(/(\.map$)|(^(main\.|favicon\.))/.test(assetFilename)),
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/app/utils/request/base/index.test.js:
--------------------------------------------------------------------------------
1 | import { extractJson, submitRequest } from './';
2 |
3 | const Auth = require('../../auth');
4 |
5 | describe('#submitRequest', () => {
6 | describe('success', () => {
7 | const dummyResponse = new Response('{"hello":"world"}', {
8 | status: 200,
9 | headers: {
10 | 'Content-type': 'application/json',
11 | },
12 | });
13 |
14 | let actualResponse;
15 |
16 | beforeEach(() => {
17 | window.fetch = jest.fn();
18 | window.fetch.mockReturnValue(Promise.resolve(dummyResponse));
19 | });
20 |
21 | describe('no headers, no data', () => {
22 | const url = 'https://api.github.com/users/alexander-elgin/repos?type=all&sort=updated';
23 | const method = 'GET';
24 |
25 | beforeEach((done) => {
26 | Auth.isTokenSet = jest.fn().mockReturnValue(true);
27 |
28 | submitRequest(url, method, false).then((response) => {
29 | actualResponse = response;
30 | done();
31 | });
32 | });
33 |
34 | it('submits a request without Authorization header', () => expect(window.fetch).toBeCalledWith(url, {
35 | method,
36 | headers: {},
37 | }));
38 | });
39 |
40 | describe('with headers, with data', () => {
41 | const url = '/auth/signin';
42 | const method = 'POST';
43 | const data = {
44 | username: 'alexander-elgin',
45 | password: '123',
46 | };
47 |
48 | beforeEach((done) => submitRequest(url, method, true, data).then((response) => {
49 | actualResponse = response;
50 | done();
51 | }));
52 |
53 | it('submits a request', () => expect(window.fetch).toBeCalledWith(url, {
54 | body: JSON.stringify(data),
55 | method,
56 | headers: {
57 | Accept: 'application/json',
58 | 'Content-Type': 'application/json',
59 | },
60 | }));
61 | });
62 |
63 | afterEach(() => expect(actualResponse).toEqual(dummyResponse));
64 | });
65 | });
66 |
67 | describe('#extractJson', () => {
68 | describe('success', () => {
69 | const payload = { hello: 'world' };
70 |
71 | it('resolves JSON response', () => {
72 | expect(extractJson(Promise.resolve(new Response(JSON.stringify(payload), {
73 | status: 200,
74 | headers: {
75 | 'Content-type': 'application/json',
76 | },
77 | })))).resolves.toEqual(payload);
78 | });
79 | });
80 |
81 | describe('failure', () => {
82 | const errorDescription = 'Not Found';
83 |
84 | it('rejects with an error', (done) => {
85 | extractJson(Promise.resolve(new Response(null, {
86 | status: 404,
87 | statusText: errorDescription,
88 | headers: {
89 | 'Content-type': 'application/json',
90 | },
91 | }))).then(null, (error) => {
92 | expect(error).toEqual(new Error(errorDescription));
93 | done();
94 | });
95 | });
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/app/pages/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
3 | import { Field, Form, Formik } from 'formik';
4 |
5 | import messages from './messages';
6 | import RepositoriesList from './RepositoriesList';
7 | import styles from './styles.scss';
8 | import H2 from '../../atoms/H2';
9 | import Page from '../../molecules/Page';
10 | import { get } from '../../utils/request';
11 |
12 | const HomePage = ({ intl }) => {
13 | const [currentUsername, setCurrentUsername] = useState(undefined);
14 | const [error, setError] = useState(false);
15 | const [repos, setRepos] = useState([]);
16 |
17 | const onSubmitForm = async ({ username }) => {
18 | setError(false);
19 | setRepos([]);
20 |
21 | try {
22 | const repositories = await get(
23 | `/users/${username}/repos`,
24 | { type: 'all', sort: 'updated' },
25 | 'https://api.github.com'
26 | );
27 |
28 | setRepos(repositories);
29 | setCurrentUsername(username);
30 | } catch (e) {
31 | setError(true);
32 | }
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ({})} onSubmit={(data) => onSubmitForm(data)}>
50 | {({ handleSubmit, isSubmitting, setFieldValue }) => {
51 | let timeout;
52 |
53 | const triggerSubmit = ({ target }) => {
54 | if (timeout !== undefined) {
55 | clearTimeout(timeout);
56 | }
57 |
58 | timeout = setTimeout(handleSubmit, 1500);
59 | setFieldValue('username', target.value);
60 | };
61 |
62 | return (
63 |
64 |
75 |
76 |
77 | );
78 | }}
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | HomePage.propTypes = {
86 | intl: intlShape.isRequired,
87 | };
88 |
89 | export default injectIntl(HomePage);
90 |
--------------------------------------------------------------------------------
/app/organisms/App/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Enzyme, { shallow } from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 | import { Route, Switch } from 'react-router-dom';
5 | import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles';
6 |
7 | import DefaultTemplate from '../../templates/default';
8 |
9 | import NotFoundPage from '../../pages/404-NotFound';
10 | import HomePage from '../../pages/Home';
11 | import FeatureListPage from '../../pages/FeatureList';
12 |
13 | import SignInPage from '../../pages/SignIn';
14 | import SignOutPage from '../../pages/SignOut';
15 | import SignUpPage from '../../pages/SignUp';
16 |
17 | import UnauthorizedRoute from './UnauthorizedRoute';
18 |
19 | import App from './';
20 |
21 | Enzyme.configure({ adapter: new Adapter() });
22 |
23 | const defaultTheme = require('./defaultTheme');
24 |
25 | describe(' ', () => {
26 | const DUMMY_THEME = { dummyField: 'Dummy Theme' };
27 | let app;
28 |
29 | beforeEach(() => {
30 | defaultTheme.default = DUMMY_THEME;
31 | app = shallow( );
32 | });
33 |
34 | it('renders Material UI Theme Provider', () => {
35 | expect(app.type()).toEqual(MuiThemeProvider);
36 | expect(app.prop('theme')).toEqual(DUMMY_THEME);
37 | });
38 |
39 | describe('routes', () => {
40 | let routeSwitch;
41 |
42 | beforeEach(() => {
43 | routeSwitch = app.find(Switch).first();
44 | });
45 |
46 | const checkRoute = (description, index, path, isExactPathRequired, ContentComponent, RouteType) => {
47 | describe(description, () => {
48 | const className = 'component-class';
49 | let route;
50 | let template;
51 |
52 | beforeEach(() => {
53 | route = routeSwitch.childAt(index);
54 | const Template = route.prop('component');
55 | template = shallow( );
56 | });
57 |
58 | it('passes props to the content component', () => expect(template.prop('className')).toBe(className));
59 | it('uses the default template', () => expect(template.type()).toEqual(DefaultTemplate));
60 | it('uses a particular route type', () => expect(route.type()).toEqual(RouteType));
61 | it('renders on custom path', () => expect(route.prop('path')).toBe(path));
62 | it('renders the page content', () => expect(template.prop('component')).toEqual(ContentComponent));
63 |
64 | it(isExactPathRequired ? 'is activated on exact path match only' : 'does not require exact path match', () => {
65 | expect(route.prop('exact'))[isExactPathRequired ? 'toBeDefined' : 'toBeUndefined']();
66 | });
67 | });
68 | };
69 |
70 | checkRoute('home', 0, '/', true, HomePage, Route);
71 | checkRoute('features list', 1, '/features', false, FeatureListPage, Route);
72 | checkRoute('sign in', 2, '/signin', false, SignInPage, UnauthorizedRoute);
73 | checkRoute('sign up', 3, '/signup', false, SignUpPage, UnauthorizedRoute);
74 | checkRoute('sign out', 4, '/signout', false, SignOutPage, Route);
75 | checkRoute('not found', 5, '', false, NotFoundPage, Route);
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/app/translations/de.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | "use strict";
3 |
4 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
5 |
6 | exports.__esModule = true;
7 | exports.default = exports.array = exports.object = exports.boolean = exports.date = exports.number = exports.string = exports.mixed = void 0;
8 |
9 | var _printValue = _interopRequireDefault(require("yup/lib/util/printValue"));
10 |
11 | var mixed = {
12 | default: '${path} ist ungültig',
13 | required: '${path} ist ein Pflichtfeld',
14 | oneOf: '${path} muss einer der folgenden Werte sein: ${values}',
15 | notOneOf: '${path} darf keiner der folgenden Werte sein: ${values}',
16 | notType: function notType(_ref) {
17 | var path = _ref.path,
18 | type = _ref.type,
19 | value = _ref.value,
20 | originalValue = _ref.originalValue;
21 | var isCast = originalValue != null && originalValue !== value;
22 | var msg = path + " muss ein `" + type + "` Typ sein, " + ("aber der endgültige Wert war: `" +
23 | (0, _printValue.default)(value, true) + "`") + (isCast ? " (aber der endgültige Wert war `" +
24 | (0, _printValue.default)(originalValue, true) + "`)." : '.');
25 |
26 | if (value === null) {
27 | msg += "\n Wenn \"null\" als leerer Wert vorgesehen ist, müssen Sie das Schema als `.nullable ()` kennzeichnen";
28 | }
29 |
30 | return msg;
31 | }
32 | };
33 | exports.mixed = mixed;
34 | var string = {
35 | length: '${path} muss genau ${length} Zeichen sein',
36 | min: '${path} muss mindestens ${min} Zeichen enthalten',
37 | max: '${path} darf höchstens ${max} Zeichen enthalten',
38 | matches: '${path} muss mit folgendem übereinstimmen: "${regex}"',
39 | email: '${path} muss eine gültige E-Mail-Adresse sein',
40 | url: '${path} muss eine gültige URL sein',
41 | trim: '${path} muss eine gekürzte Zeichenfolge sein',
42 | lowercase: '${path} muss ein Kleinbuchstabe sein',
43 | uppercase: '${path} muss ein Großbuchstabe sein'
44 | };
45 | exports.string = string;
46 | var number = {
47 | min: '${path} muss größer oder gleich ${min} sein',
48 | max: '${path} muss kleiner oder gleich ${max} sein',
49 | lessThan: '${path} muss kleiner als ${less} sein',
50 | moreThan: '${path} muss größer als ${more} sein',
51 | notEqual: '${path} darf nicht gleich ${notEqual} sein',
52 | positive: '${path} muss eine positive Zahl sein',
53 | negative: '${path} muss eine negative Zahl sein',
54 | integer: '${path} muss eine ganze Zahl sein'
55 | };
56 | exports.number = number;
57 | var date = {
58 | min: '${path} Feld muss später als ${min} sein',
59 | max: '${path} Feld muss vor ${max} liegen'
60 | };
61 | exports.date = date;
62 | var boolean = {};
63 | exports.boolean = boolean;
64 | var object = {
65 | noUnknown: 'Das Feld ${path} darf keine Schlüssel enthalten, die nicht in der Objektform angegeben sind'
66 | };
67 | exports.object = object;
68 | var array = {
69 | min: 'Das Feld ${path} muss mindestens ${min} Elemente enthalten',
70 | max: 'Das Feld ${path} darf höchstens ${max} Elemente enthalten'
71 | };
72 | exports.array = array;
73 | var _default = {
74 | mixed: mixed,
75 | string: string,
76 | number: number,
77 | date: date,
78 | object: object,
79 | array: array,
80 | boolean: boolean
81 | };
82 | exports.default = _default;
83 |
--------------------------------------------------------------------------------
/app/.nginx.conf:
--------------------------------------------------------------------------------
1 | ##
2 | # Put this file in /etc/nginx/conf.d folder and make sure
3 | # you have a line 'include /etc/nginx/conf.d/*.conf;'
4 | # in your main nginx configuration file
5 | ##
6 |
7 | ##
8 | # Redirect to the same URL with https://
9 | ##
10 |
11 | server {
12 |
13 | listen 80;
14 |
15 | # Type your domain name below
16 | server_name example.com;
17 |
18 | return 301 https://$server_name$request_uri;
19 |
20 | }
21 |
22 | ##
23 | # HTTPS configurations
24 | ##
25 |
26 | server {
27 |
28 | listen 443 ssl;
29 |
30 | # Type your domain name below
31 | server_name example.com;
32 |
33 | # Configure the Certificate and Key you got from your CA (e.g. Lets Encrypt)
34 | ssl_certificate /path/to/certificate.crt;
35 | ssl_certificate_key /path/to/server.key;
36 |
37 | ssl_session_timeout 1d;
38 | ssl_session_cache shared:SSL:50m;
39 | ssl_session_tickets off;
40 |
41 | # Only use TLS v1.2 as Transport Security Protocol
42 | ssl_protocols TLSv1.2;
43 |
44 | # Only use ciphersuites that are considered modern and secure by Mozilla
45 | ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
46 |
47 | # Do not let attackers downgrade the ciphersuites in Client Hello
48 | # Always use server-side offered ciphersuites
49 | ssl_prefer_server_ciphers on;
50 |
51 | # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
52 | add_header Strict-Transport-Security max-age=15768000;
53 |
54 | # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
55 | # Uncomment if you want to use your own Diffie-Hellman parameter, which can be generated with: openssl ecparam -genkey -out dhparam.pem -name prime256v1
56 | # See https://wiki.mozilla.org/Security/Server_Side_TLS#DHE_handshake_and_dhparam
57 | # ssl_dhparam /path/to/dhparam.pem;
58 |
59 |
60 | ## OCSP Configuration START
61 | # If you want to provide OCSP Stapling, you can uncomment the following lines
62 | # See https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx for more infos about OCSP and its use case
63 | # fetch OCSP records from URL in ssl_certificate and cache them
64 |
65 | #ssl_stapling on;
66 | #ssl_stapling_verify on;
67 |
68 | # verify chain of trust of OCSP response using Root CA and Intermediate certs (you will get this file from your CA)
69 | #ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;
70 |
71 | ## OCSP Configuration END
72 |
73 | # To let nginx use its own DNS Resolver
74 | # resolver ;
75 |
76 |
77 | # Always serve index.html for any request
78 | location / {
79 | # Set path
80 | root /var/www/;
81 | try_files $uri /index.html;
82 | }
83 |
84 | # Do not cache sw.js, required for offline-first updates.
85 | location /sw.js {
86 | add_header Cache-Control "no-cache";
87 | proxy_cache_bypass $http_pragma;
88 | proxy_cache_revalidate on;
89 | expires off;
90 | access_log off;
91 | }
92 |
93 | ##
94 | # If you want to use Node/Rails/etc. API server
95 | # on the same port (443) config Nginx as a reverse proxy.
96 | # For security reasons use a firewall like ufw in Ubuntu
97 | # and deny port 3000/tcp.
98 | ##
99 |
100 | # location /api/ {
101 | #
102 | # proxy_pass http://localhost:3000;
103 | # proxy_http_version 1.1;
104 | # proxy_set_header X-Forwarded-Proto https;
105 | # proxy_set_header Upgrade $http_upgrade;
106 | # proxy_set_header Connection 'upgrade';
107 | # proxy_set_header Host $host;
108 | # proxy_cache_bypass $http_upgrade;
109 | #
110 | # }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/__snapshots__/index.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders the Features List page 1`] = `
4 |
5 |
8 |
9 | Features
10 |
11 |
12 |
15 |
16 |
17 |
20 |
21 | Quick scaffolding
22 |
23 |
24 |
27 |
28 | Automate the creation of components, containers, routes, selectors
29 | and sagas - and their tests - right from the CLI!
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 | Quick scaffolding
41 |
42 |
43 |
46 |
47 | Automate the creation of components, containers, routes, selectors
48 | and sagas - and their tests - right from the CLI!
49 |
50 |
51 |
52 |
53 |
54 |
55 |
58 |
59 | Instant feedback
60 |
61 |
62 |
65 |
66 |
67 | Enjoy the best DX and code your app at the speed of thought! Your
68 | saved changes to the CSS and JS are reflected instantaneously
69 | without refreshing the page. Preserve application state even when
70 | you update something in the underlying code!
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
81 |
82 | Industry-standard routing
83 |
84 |
85 |
88 |
89 |
90 | Write composable CSS that's co-located with your components for
91 | complete modularity. Unique generated class names keep the
92 | specificity low while eliminating style clashes. Ship only the
93 | styles that are on the page for the best performance.
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
104 |
105 | Offline-first
106 |
107 |
108 |
111 |
112 |
113 | The next frontier in performant web apps: availability without a
114 | network connection from the instant your users load the app.
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
125 |
126 | Complete i18n Standard Internationalization & Pluralization
127 |
128 |
129 |
132 |
133 | Scalable apps need to support multiple languages, easily add and support multiple languages with \`react-intl\`.
134 |
135 |
136 |
137 |
138 |
139 |
140 | `;
141 |
--------------------------------------------------------------------------------
/app/pages/FeatureList/messages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Feature List Page Messages
3 | *
4 | * This contains all the text for the FeaturePage component.
5 | */
6 | import { defineMessages } from 'react-intl';
7 |
8 | export default defineMessages({
9 | metaTitle: {
10 | id: 'boilerplate.pages.FeatureList.metaTitle',
11 | defaultMessage: 'Features List',
12 | },
13 | metaDescription: {
14 | id: 'boilerplate.pages.FeatureList.metaDescription',
15 | defaultMessage: 'Features list of React.js Boilerplate application',
16 | },
17 | header: {
18 | id: 'boilerplate.pages.FeatureList.header',
19 | defaultMessage: 'Features',
20 | },
21 | scaffoldingHeader: {
22 | id: 'boilerplate.pages.FeatureList.scaffolding.header',
23 | defaultMessage: 'Quick scaffolding',
24 | },
25 | scaffoldingMessage: {
26 | id: 'boilerplate.pages.FeatureList.scaffolding.message',
27 | defaultMessage: `Automate the creation of components, containers, routes, selectors
28 | and sagas - and their tests - right from the CLI!`,
29 | },
30 | feedbackHeader: {
31 | id: 'boilerplate.pages.FeatureList.feedback.header',
32 | defaultMessage: 'Instant feedback',
33 | },
34 | feedbackMessage: {
35 | id: 'boilerplate.pages.FeatureList.feedback.message',
36 | defaultMessage: `
37 | Enjoy the best DX and code your app at the speed of thought! Your
38 | saved changes to the CSS and JS are reflected instantaneously
39 | without refreshing the page. Preserve application state even when
40 | you update something in the underlying code!
41 | `,
42 | },
43 | stateManagementHeader: {
44 | id: 'boilerplate.pages.FeatureList.state_management.header',
45 | defaultMessage: 'Predictable state management',
46 | },
47 | stateManagementMessages: {
48 | id: 'boilerplate.pages.FeatureList.state_management.message',
49 | defaultMessage: `
50 | Unidirectional data flow allows for change logging and time travel
51 | debugging.
52 | `,
53 | },
54 | javascriptHeader: {
55 | id: 'boilerplate.pages.FeatureList.javascript.header',
56 | defaultMessage: 'Next generation JavaScript',
57 | },
58 | javascriptMessage: {
59 | id: 'boilerplate.pages.FeatureList.javascript.message',
60 | defaultMessage: `Use template strings, object destructuring, arrow functions, JSX
61 | syntax and more, today.`,
62 | },
63 | cssHeader: {
64 | id: 'boilerplate.pages.FeatureList.css.header',
65 | defaultMessage: 'Features',
66 | },
67 | cssMessage: {
68 | id: 'boilerplate.pages.FeatureList.css.message',
69 | defaultMessage: 'Next generation CSS',
70 | },
71 | routingHeader: {
72 | id: 'boilerplate.pages.FeatureList.routing.header',
73 | defaultMessage: 'Industry-standard routing',
74 | },
75 | routingMessage: {
76 | id: 'boilerplate.pages.FeatureList.routing.message',
77 | defaultMessage: `
78 | Write composable CSS that's co-located with your components for
79 | complete modularity. Unique generated class names keep the
80 | specificity low while eliminating style clashes. Ship only the
81 | styles that are on the page for the best performance.
82 | `,
83 | },
84 | networkHeader: {
85 | id: 'boilerplate.pages.FeatureList.network.header',
86 | defaultMessage: 'Offline-first',
87 | },
88 | networkMessage: {
89 | id: 'boilerplate.pages.FeatureList.network.message',
90 | defaultMessage: `
91 | The next frontier in performant web apps: availability without a
92 | network connection from the instant your users load the app.
93 | `,
94 | },
95 | intlHeader: {
96 | id: 'boilerplate.pages.FeatureList.internationalization.header',
97 | defaultMessage: 'Complete i18n Standard Internationalization & Pluralization',
98 | },
99 | intlMessage: {
100 | id: 'boilerplate.pages.FeatureList.internationalization.message',
101 | defaultMessage: 'Scalable apps need to support multiple languages, easily add and support multiple languages with `react-intl`.',
102 | },
103 | });
104 |
--------------------------------------------------------------------------------
/app/organisms/SignInForm/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Link, useHistory } from 'react-router-dom';
4 | import { ErrorMessage, Field, Form, Formik } from 'formik';
5 | import { TextField } from 'formik-material-ui';
6 | import {
7 | Button,
8 | Card,
9 | CardActions,
10 | CardContent,
11 | CardHeader,
12 | Grid,
13 | InputAdornment,
14 | LinearProgress,
15 | } from '@material-ui/core';
16 | import {
17 | Email,
18 | Lock,
19 | } from '@material-ui/icons';
20 | import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
21 | import * as yup from 'yup';
22 |
23 | import messages from './messages';
24 | import Error from '../../atoms/Error';
25 | import { setSignInData } from '../../store/auth/actions';
26 | import { setToken } from '../../utils/auth';
27 | import submitForm from '../../utils/form';
28 | import { post } from '../../utils/request';
29 |
30 | const SignInForm = ({ intl }) => {
31 | const { push } = useHistory();
32 | const dispatch = useDispatch();
33 |
34 | const validationSchema = yup.object().shape({
35 | email: yup.string().required().email().label(intl.formatMessage(messages.email)),
36 | password: yup.string().required().min(8).label(intl.formatMessage(messages.password)),
37 | });
38 |
39 | return (
40 | submitForm(
44 | () => post('/auth/signin', values),
45 | ({ token, user }) => {
46 | dispatch(setSignInData(user));
47 | setToken(token);
48 | push('/features');
49 | },
50 | intl, messages, formActions
51 | )}
52 | >
53 | {({ isSubmitting }) => (
54 |
104 | )}
105 |
106 | );
107 | };
108 |
109 | SignInForm.propTypes = {
110 | intl: intlShape.isRequired,
111 | };
112 |
113 | export default injectIntl(SignInForm);
114 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.base.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * COMMON WEBPACK CONFIGURATION
3 | */
4 |
5 | require('dotenv-flow').config();
6 | const path = require('path');
7 | const webpack = require('webpack');
8 |
9 | const getAppEnv = (variables) => Object.keys(variables)
10 | .filter((variable) => variable.indexOf('APP_ENV_') === 0)
11 | .reduce((env, variable) => ({ ...env, [variable]: JSON.stringify(variables[variable]) }), {});
12 |
13 | // Remove this line once the following warning goes away (it was meant for webpack loader authors not users):
14 | // 'DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic,
15 | // see https://github.com/webpack/loader-utils/issues/56 parseQuery() will be replaced with getOptions()
16 | // in the next major version of loader-utils.'
17 | process.noDeprecation = true;
18 |
19 | module.exports = (options) => ({
20 | entry: options.entry,
21 | node: {
22 | fs: 'empty',
23 | },
24 | output: Object.assign({ // Compile into js/build.js
25 | path: path.resolve(process.cwd(), 'build'),
26 | }, options.output), // Merge with env dependent settings
27 | module: {
28 | rules: [
29 | {
30 | test: /\.jsx?$/, // Transform all .js and .jsx files required somewhere with Babel
31 | exclude: /node_modules/,
32 | use: {
33 | loader: 'babel-loader',
34 | options: options.babelQuery,
35 | },
36 | },
37 | {
38 | // Preprocess our own .css files
39 | // This is the place to add your own loaders (e.g. sass/less etc.)
40 | // for a list of loaders, see https://webpack.js.org/loaders/#styling
41 | test: /\.scss$/,
42 | exclude: /node_modules/,
43 | use: [
44 | 'style-loader',
45 | {
46 | loader: 'css-loader',
47 | options: {
48 | modules: true,
49 | },
50 | },
51 | 'postcss-loader',
52 | ],
53 | },
54 | {
55 | // Preprocess 3rd party .css files located in node_modules
56 | test: /\.css$/,
57 | include: /node_modules/,
58 | use: ['style-loader', 'css-loader'],
59 | },
60 | {
61 | test: /\.(eot|svg|otf|ttf|woff|woff2)$/,
62 | use: 'file-loader',
63 | },
64 | {
65 | test: /\.(jpg|png|gif)$/,
66 | use: [
67 | 'file-loader',
68 | {
69 | loader: 'image-webpack-loader',
70 | options: {
71 | progressive: true,
72 | optimizationLevel: 7,
73 | interlaced: false,
74 | pngquant: {
75 | quality: '65-90',
76 | speed: 4,
77 | },
78 | },
79 | },
80 | ],
81 | },
82 | {
83 | test: /\.html$/,
84 | use: 'html-loader',
85 | },
86 | {
87 | test: /\.json$/,
88 | use: 'json-loader',
89 | },
90 | {
91 | test: /\.(mp4|webm)$/,
92 | use: {
93 | loader: 'url-loader',
94 | options: {
95 | limit: 10000,
96 | },
97 | },
98 | },
99 | ],
100 | },
101 | plugins: options.plugins.concat([
102 | new webpack.ProvidePlugin({
103 | // make fetch available
104 | fetch: 'exports-loader?self.fetch!whatwg-fetch',
105 | }),
106 |
107 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV`
108 | // inside your code for any environment checks; UglifyJS will automatically
109 | // drop any unreachable code.
110 | new webpack.DefinePlugin({
111 | ...getAppEnv(process.env),
112 | 'process.env': {
113 | NODE_ENV: JSON.stringify(process.env.NODE_ENV),
114 | },
115 | }),
116 | new webpack.NamedModulesPlugin(),
117 | ]),
118 | resolve: {
119 | modules: ['app', 'node_modules'],
120 | extensions: [
121 | '.js',
122 | '.jsx',
123 | '.react.js',
124 | ],
125 | mainFields: [
126 | 'browser',
127 | 'jsnext:main',
128 | 'main',
129 | ],
130 | },
131 | devtool: options.devtool,
132 | target: 'web', // Make web variables accessible to webpack, e.g. window
133 | performance: options.performance || {},
134 | });
135 |
--------------------------------------------------------------------------------
/app/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * app.js
3 | *
4 | * This is the entry file for the application, only setup and boilerplate
5 | * code.
6 | */
7 |
8 | // Needed for es6 generator support
9 | import 'babel-polyfill';
10 |
11 | // Import all the third party stuff
12 | import React from 'react';
13 | import ReactDOM from 'react-dom';
14 | import { Provider } from 'react-redux';
15 | import { Router } from 'react-router-dom';
16 | import FontFaceObserver from 'fontfaceobserver';
17 | import { createBrowserHistory as createHistory } from 'history';
18 | import 'sanitize.css/sanitize.css';
19 |
20 | // Import root app
21 | import App from 'organisms/App';
22 |
23 | // Import Language Provider
24 | import LanguageProvider from 'organisms/LanguageProvider';
25 |
26 | // Load the favicon, the manifest.json file and the .htaccess file
27 | /* eslint-disable import/no-webpack-loader-syntax */
28 | import '!file-loader?name=[name].[ext]!./images/favicon.ico';
29 | import '!file-loader?name=[name].[ext]!./images/icon-72x72.png';
30 | import '!file-loader?name=[name].[ext]!./images/icon-96x96.png';
31 | import '!file-loader?name=[name].[ext]!./images/icon-120x120.png';
32 | import '!file-loader?name=[name].[ext]!./images/icon-128x128.png';
33 | import '!file-loader?name=[name].[ext]!./images/icon-144x144.png';
34 | import '!file-loader?name=[name].[ext]!./images/icon-152x152.png';
35 | import '!file-loader?name=[name].[ext]!./images/icon-167x167.png';
36 | import '!file-loader?name=[name].[ext]!./images/icon-180x180.png';
37 | import '!file-loader?name=[name].[ext]!./images/icon-192x192.png';
38 | import '!file-loader?name=[name].[ext]!./images/icon-384x384.png';
39 | import '!file-loader?name=[name].[ext]!./images/icon-512x512.png';
40 | import '!file-loader?name=[name].[ext]!./manifest.json';
41 | import 'file-loader?name=[name].[ext]!./.htaccess'; // eslint-disable-line import/extensions
42 | /* eslint-enable import/no-webpack-loader-syntax */
43 |
44 | import translationMessages from './i18n';
45 | import reducers from './reducers';
46 | import { DEFAULT_LOCALE } from './store/language/constants';
47 | import configureStore from './utils/store';
48 | import setYupLocale from './utils/yup';
49 |
50 | // Observe loading of Open Sans (to remove open sans, remove the tag in
51 | // the index.html file and this observer)
52 | const openSansObserver = new FontFaceObserver('Open Sans', {});
53 |
54 | // When Open Sans is loaded, add a font-family using Open Sans to the body
55 | openSansObserver.load().then(() => {
56 | document.body.classList.add('fontLoaded');
57 | }, () => {
58 | document.body.classList.remove('fontLoaded');
59 | });
60 |
61 | setYupLocale(DEFAULT_LOCALE);
62 | const history = createHistory({ basename: APP_ENV_BASE_PATH });
63 | const store = configureStore(reducers);
64 | const MOUNT_NODE = document.getElementById('app');
65 |
66 | const render = (messages) => {
67 | ReactDOM.render(
68 |
69 |
70 |
71 |
72 |
73 |
74 | ,
75 | MOUNT_NODE
76 | );
77 | };
78 |
79 | if (module.hot) {
80 | // Hot reloadable React components and translation json files
81 | // modules.hot.accept does not accept dynamic dependencies,
82 | // have to be constants at compile-time
83 | module.hot.accept(['./i18n', 'organisms/App'], () => {
84 | ReactDOM.unmountComponentAtNode(MOUNT_NODE);
85 | render(translationMessages);
86 | });
87 | }
88 |
89 | // Chunked polyfill for browsers without Intl support
90 | if (!window.Intl) {
91 | (new Promise((resolve) => {
92 | resolve(import('intl'));
93 | }))
94 | .then(() => Promise.all(Object.keys(translationMessages).map((langIso2Code) =>
95 | import(`intl/locale-data/jsonp/${langIso2Code}.js`)
96 | )))
97 | .then(() => render(translationMessages))
98 | .catch((err) => {
99 | throw err;
100 | });
101 | } else {
102 | render(translationMessages);
103 | }
104 |
105 | // Install ServiceWorker and AppCache in the end since
106 | // it's not most important operation and if main code fails,
107 | // we do not want it installed
108 | if (process.env.NODE_ENV === 'production') {
109 | require('offline-plugin/runtime').install(); // eslint-disable-line global-require
110 | }
111 |
--------------------------------------------------------------------------------
/app/organisms/SignUpForm/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, useHistory } from 'react-router-dom';
3 | import { ErrorMessage, Field, Form, Formik } from 'formik';
4 | import { TextField } from 'formik-material-ui';
5 | import {
6 | Button,
7 | Card,
8 | CardActions,
9 | CardContent,
10 | CardHeader,
11 | Grid,
12 | InputAdornment,
13 | LinearProgress,
14 | } from '@material-ui/core';
15 | import {
16 | Email,
17 | Lock,
18 | Person,
19 | } from '@material-ui/icons';
20 | import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
21 | import * as yup from 'yup';
22 |
23 | import submitForm from '../../utils/form';
24 | import { post } from '../../utils/request';
25 | import Error from '../../atoms/Error';
26 | import messages from './messages';
27 |
28 | const SignUpForm = ({ intl }) => {
29 | const { push } = useHistory();
30 |
31 | const validationSchema = yup.object().shape({
32 | email: yup.string().required().email().label(intl.formatMessage(messages.email)),
33 | name: yup.string().required().label(intl.formatMessage(messages.name)),
34 | password: yup.string().required().min(8).label(intl.formatMessage(messages.password)),
35 | });
36 |
37 | return (
38 | submitForm(
42 | () => post('/auth/signup', values),
43 | () => push('/signin'),
44 | intl, messages, formActions
45 | )}
46 | >
47 | {({ isSubmitting }) => (
48 |
111 | )}
112 |
113 | );
114 | };
115 |
116 | SignUpForm.propTypes = {
117 | intl: intlShape.isRequired,
118 | };
119 |
120 | export default injectIntl(SignUpForm);
121 |
--------------------------------------------------------------------------------
/internals/webpack/webpack.dev.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * DEVELOPMENT WEBPACK CONFIGURATION
3 | */
4 |
5 | const path = require('path');
6 | const fs = require('fs');
7 | const glob = require('glob');
8 | const webpack = require('webpack');
9 | const HtmlWebpackPlugin = require('html-webpack-plugin');
10 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
11 | const CircularDependencyPlugin = require('circular-dependency-plugin');
12 | const logger = require('../../server/logger');
13 | const pkg = require(path.resolve(process.cwd(), 'package.json'));
14 | const dllPlugin = pkg.dllPlugin;
15 |
16 | const plugins = [
17 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading
18 | new webpack.NoEmitOnErrorsPlugin(),
19 | new HtmlWebpackPlugin({
20 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js
21 | template: 'app/index.html',
22 | }),
23 | new CircularDependencyPlugin({
24 | exclude: /a\.js|node_modules/, // exclude node_modules
25 | failOnError: false, // show a warning when there is a circular dependency
26 | }),
27 | ];
28 |
29 | if (dllPlugin) {
30 | glob.sync(`${dllPlugin.path}/*.dll.js`).forEach((dllPath) => {
31 | plugins.push(
32 | new AddAssetHtmlPlugin({
33 | filepath: dllPath,
34 | includeSourcemap: false,
35 | })
36 | );
37 | });
38 | }
39 |
40 | module.exports = require('./webpack.base.babel')({
41 | // Add hot reloading in development
42 | entry: [
43 | 'eventsource-polyfill', // Necessary for hot reloading with IE
44 | 'webpack-hot-middleware/client?reload=true',
45 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js
46 | ],
47 |
48 | // Don't use hashes in dev mode for better performance
49 | output: {
50 | filename: '[name].js',
51 | chunkFilename: '[name].chunk.js',
52 | },
53 |
54 | // Add development plugins
55 | plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define
56 |
57 | // Emit a source map for easier debugging
58 | // See https://webpack.js.org/configuration/devtool/#devtool
59 | devtool: 'eval-source-map',
60 |
61 | performance: {
62 | hints: false,
63 | },
64 | });
65 |
66 | /**
67 | * Select which plugins to use to optimize the bundle's handling of
68 | * third party dependencies.
69 | *
70 | * If there is a dllPlugin key on the project's package.json, the
71 | * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin
72 | * will be used.
73 | *
74 | */
75 | function dependencyHandlers() {
76 | // Don't do anything during the DLL Build step
77 | if (process.env.BUILDING_DLL) { return []; }
78 |
79 | // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin
80 | if (!dllPlugin) {
81 | return [
82 | new webpack.optimize.CommonsChunkPlugin({
83 | name: 'vendor',
84 | children: true,
85 | minChunks: 2,
86 | async: true,
87 | }),
88 | ];
89 | }
90 |
91 | const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls');
92 |
93 | /**
94 | * If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json
95 | * Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude
96 | */
97 | if (!dllPlugin.dlls) {
98 | const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json');
99 |
100 | if (!fs.existsSync(manifestPath)) {
101 | logger.error('The DLL manifest is missing. Please run `npm run build:dll`');
102 | process.exit(0);
103 | }
104 |
105 | return [
106 | new webpack.DllReferencePlugin({
107 | context: process.cwd(),
108 | manifest: require(manifestPath), // eslint-disable-line global-require
109 | }),
110 | ];
111 | }
112 |
113 | // If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them.
114 | const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`));
115 |
116 | return dllManifests.map((manifestPath) => {
117 | if (!fs.existsSync(path)) {
118 | if (!fs.existsSync(manifestPath)) {
119 | logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`);
120 | logger.error(`Expected to find it in ${dllPath}`);
121 | logger.error('Please run: npm run build:dll');
122 |
123 | process.exit(0);
124 | }
125 | }
126 |
127 | return new webpack.DllReferencePlugin({
128 | context: process.cwd(),
129 | manifest: require(manifestPath), // eslint-disable-line global-require
130 | });
131 | });
132 | }
133 |
--------------------------------------------------------------------------------
/app/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "boilerplate.organisms.Footer.author.message": "Made with love by {author}.",
3 | "boilerplate.organisms.Footer.license.message": "This project is licensed under the MIT license.",
4 | "boilerplate.organisms.Header.features": "Features",
5 | "boilerplate.organisms.Header.home": "Home",
6 | "boilerplate.organisms.Header.logo": "Logo",
7 | "boilerplate.organisms.LocaleSwitcher.de": "de",
8 | "boilerplate.organisms.LocaleSwitcher.en": "en",
9 | "boilerplate.organisms.SignInForm.noAccount": "Don't have an account?",
10 | "boilerplate.organisms.SignUpForm.alreadyHaveAccount": "Already have an account?",
11 | "boilerplate.pages.FeatureList.css.header": "Features",
12 | "boilerplate.pages.FeatureList.css.message": "Next generation CSS",
13 | "boilerplate.pages.FeatureList.feedback.header": "Instant feedback",
14 | "boilerplate.pages.FeatureList.feedback.message": "Enjoy the best DX and code your app at the speed of thought! Your\n saved changes to the CSS and JS are reflected instantaneously\n without refreshing the page. Preserve application state even when\n you update something in the underlying code!",
15 | "boilerplate.pages.FeatureList.metaTitle": "Features List",
16 | "boilerplate.pages.FeatureList.metaDescription": "Features list of React.js Boilerplate application",
17 | "boilerplate.pages.FeatureList.header": "Features",
18 | "boilerplate.pages.FeatureList.internationalization.header": "Complete i18n Standard Internationalization & Pluralization",
19 | "boilerplate.pages.FeatureList.internationalization.message": "Scalable apps need to support multiple languages, easily add and support multiple languages with `react-intl`.",
20 | "boilerplate.pages.FeatureList.javascript.header": "Next generation JavaScript",
21 | "boilerplate.pages.FeatureList.javascript.message": "Use template strings, object destructuring, arrow functions, JSX\n syntax and more, today.",
22 | "boilerplate.pages.FeatureList.network.header": "Offline-first",
23 | "boilerplate.pages.FeatureList.network.message": "The next frontier in performant web apps: availability without a\n network connection from the instant your users load the app.",
24 | "boilerplate.pages.FeatureList.routing.header": "Industry-standard routing",
25 | "boilerplate.pages.FeatureList.routing.message": "Write composable CSS that's co-located with your components for\n complete modularity. Unique generated class names keep the\n specificity low while eliminating style clashes. Ship only the\n styles that are on the page for the best performance.",
26 | "boilerplate.pages.FeatureList.scaffolding.header": "Quick scaffolding",
27 | "boilerplate.pages.FeatureList.scaffolding.message": "Automate the creation of components, containers, routes, selectors\n and sagas - and their tests - right from the CLI!",
28 | "boilerplate.pages.FeatureList.state_management.header": "Predictable state management",
29 | "boilerplate.pages.FeatureList.state_management.message": "Unidirectional data flow allows for change logging and time travel\n debugging.",
30 | "boilerplate.pages.Home.metaTitle": "Home",
31 | "boilerplate.pages.Home.metaDescription": "The React.js Boilerplate application homepage",
32 | "boilerplate.pages.Home.showRepositories": "Show Github repositories by",
33 | "boilerplate.pages.Home.start_project.header": "Start your next react project in seconds",
34 | "boilerplate.pages.Home.start_project.message": "A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices",
35 | "boilerplate.pages.Home.tryme.header": "Try me!",
36 | "boilerplate.pages.Home.username": "username",
37 | "boilerplate.pages.Home.RepositoriesList.somethingWrong": "Something went wrong, please try again!",
38 | "boilerplate.pages.NotFound.header": "Page not found.",
39 | "boilerplate.pages.SignOut.signOutMessage": "Signing out",
40 | "boilerplate.shared.Auth.errors.DUPLICATED_EMAIL": "The email address is already in use",
41 | "boilerplate.shared.Auth.errors.EMPTY_PASSWORD": "The password cannot be empty",
42 | "boilerplate.shared.Auth.errors.INCORRECT_CREDENTIALS": "The username / email and the password do not match",
43 | "boilerplate.shared.Auth.errors.INVALID_EMAIL": "Invalid email",
44 | "boilerplate.shared.Auth.errors.INVALID_NAME": "Invalid name",
45 | "boilerplate.shared.Auth.errors.INVALID_PASSWORD": "The password should be at least 8 characters long",
46 | "boilerplate.shared.Auth.email": "Email",
47 | "boilerplate.shared.Auth.name": "Name",
48 | "boilerplate.shared.Auth.password": "Password",
49 | "boilerplate.shared.Auth.signIn": "Sign in",
50 | "boilerplate.shared.Auth.signOut": "Sign out",
51 | "boilerplate.shared.Auth.signUp": "Sign up",
52 | "boilerplate.shared.errors.FORM_SUBMISSION_FAILED": "Unable to submit the form"
53 | }
54 |
--------------------------------------------------------------------------------
/app/translations/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "boilerplate.organisms.Footer.author.message": "Mit Liebe gemacht von {author}.",
3 | "boilerplate.organisms.Footer.license.message": "Dieses Projekt wird unter der MIT-Lizenz veröffentlicht.",
4 | "boilerplate.organisms.Header.features": "Features",
5 | "boilerplate.organisms.Header.home": "HauptSeite",
6 | "boilerplate.organisms.Header.logo": "Logo",
7 | "boilerplate.organisms.LocaleSwitcher.de": "de",
8 | "boilerplate.organisms.LocaleSwitcher.en": "en",
9 | "boilerplate.organisms.SignInForm.noAccount": "Haben Sie kein Konto?",
10 | "boilerplate.organisms.SignUpForm.alreadyHaveAccount": "Haben Sie schon ein Konto?",
11 | "boilerplate.pages.FeatureList.css.header": "",
12 | "boilerplate.pages.FeatureList.css.message": "Die nächste Generation von CSS",
13 | "boilerplate.pages.FeatureList.feedback.header": "Sofortiges Feedback",
14 | "boilerplate.pages.FeatureList.feedback.message": "Genießen Sie die beste Entwicklungserfahrung und programmieren Sie Ihre App so schnell wie noch nie! Ihre Änderungen an dem CSS und JavaScript sind sofort reflektiert, ohne die Seite aktualisieren zu müssen. So bleibt der Anwendungszustand bestehen, auch wenn Sie etwas in dem darunter liegenden Code aktualisieren!",
15 | "boilerplate.pages.FeatureList.metaTitle": "Funktionsliste",
16 | "boilerplate.pages.FeatureList.metaDescription": "Funktionsliste von React.js Boilerplate Anwendung",
17 | "boilerplate.pages.FeatureList.header": "",
18 | "boilerplate.pages.FeatureList.internationalization.header": "Komplette i18n Standard-Internationalisierung und Pluralisierung",
19 | "boilerplate.pages.FeatureList.internationalization.message": "Das Internet ist global. Mehrsprachige- und Pluralisierungsunterstützung ist entscheidend für große Web-Anwendungen.",
20 | "boilerplate.pages.FeatureList.javascript.header": "Das Internet ist global. Mehrsprachige- und Pluralisierungsunterstützung ist entscheidend für große Web-Anwendungen.",
21 | "boilerplate.pages.FeatureList.javascript.message": "Benutzen Sie ES6 template strings, object destructuring, arrow functions, JSX syntax und mehr, heute.",
22 | "boilerplate.pages.FeatureList.network.header": "",
23 | "boilerplate.pages.FeatureList.network.message": "The next frontier in performant web apps: availability without a\n network connection from the instant your users load the app.",
24 | "boilerplate.pages.FeatureList.routing.header": "Standard Routing",
25 | "boilerplate.pages.FeatureList.routing.message": "Schreiben Sie CSS, das am selben Ort wie ihre Komponenten ist. Deterministisch generierte, einzigartige Klassennamen halten die Spezifität niedrig während styling Konflikte vermieden werden. Senden Sie nur das CSS an ihre Benutzer welches dann wirklich sichtbar ist für die schnellste Performance!",
26 | "boilerplate.pages.FeatureList.scaffolding.header": "Schnelles Scaffolding",
27 | "boilerplate.pages.FeatureList.scaffolding.message": "Automatisieren Sie die Kreation von Komponenten, Containern, Routen, Selektoren und Sagas – und ihre Tests – direkt von dem Terminal!",
28 | "boilerplate.pages.FeatureList.state_management.header": "Berechenbare Stateverwaltung",
29 | "boilerplate.pages.FeatureList.state_management.message": "Unidirectional data flow erlaubt uns alle Änderungen ihrer Applikation zu loggen und time travel debugging einzusetzen.",
30 | "boilerplate.pages.Home.metaTitle": "Startseite",
31 | "boilerplate.pages.Home.metaDescription": "Die React.js Boilerplate-Anwendungsstartseite",
32 | "boilerplate.pages.Home.showRepositories": "Zeige die Github Repositories von",
33 | "boilerplate.pages.Home.start_project.header": "Beginnen Sie Ihr nächstes React Projekt in Sekunden",
34 | "boilerplate.pages.Home.start_project.message": "Ein skalierendes, offline-first Fundament mit der besten DX und einem Fokus auf Performance und bewährte Methoden",
35 | "boilerplate.pages.Home.tryme.header": "Probiere mich!",
36 | "boilerplate.pages.Home.username": "Benutzername",
37 | "boilerplate.pages.Home.RepositoriesList.somethingWrong": "Etwas ist schief gelaufen. Bitte probieren noch einmal!",
38 | "boilerplate.pages.NotFound.header": "Seite nicht gefunden.",
39 | "boilerplate.pages.SignOut.signOutMessage": "Abmeldung",
40 | "boilerplate.shared.Auth.errors.DUPLICATED_EMAIL": "Die E-Mail ist bereits vergeben",
41 | "boilerplate.shared.Auth.errors.EMPTY_PASSWORD": "Das Passwort darf nicht leer sein",
42 | "boilerplate.shared.Auth.errors.INCORRECT_CREDENTIALS": "Der Benutzername / die E-Mail-Adresse und das Passwort stimmen nicht überein",
43 | "boilerplate.shared.Auth.errors.INVALID_EMAIL": "Ungültige E-Mail",
44 | "boilerplate.shared.Auth.errors.INVALID_NAME": "Ungültiger Name",
45 | "boilerplate.shared.Auth.errors.INVALID_PASSWORD": "Das Passwort sollte mindestens 8 Zeichen lang sein",
46 | "boilerplate.shared.Auth.email": "E-Mail",
47 | "boilerplate.shared.Auth.name": "Name",
48 | "boilerplate.shared.Auth.password": "Passwort",
49 | "boilerplate.shared.Auth.signIn": "Einloggen",
50 | "boilerplate.shared.Auth.signOut": "Abmelden",
51 | "boilerplate.shared.Auth.signUp": "Registrieren",
52 | "boilerplate.shared.errors.FORM_SUBMISSION_FAILED": "Das Formular kann nicht gesendet werden"
53 | }
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atomic-react-redux",
3 | "version": "1.0.0",
4 | "description": "A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices",
5 | "keywords": [
6 | "react",
7 | "redux",
8 | "boilerplate",
9 | "redux-form",
10 | "material-ui",
11 | "storybook",
12 | "atomic design",
13 | "jest",
14 | "auth"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/alexander-elgin/atomic-react-redux.git"
19 | },
20 | "engines": {
21 | "npm": ">=3",
22 | "node": ">=5"
23 | },
24 | "author": "Alexander Elgin",
25 | "license": "MIT",
26 | "scripts": {
27 | "postinstall": "npm run build:dll",
28 | "prebuild": "npm run build:clean",
29 | "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p --progress --hide-modules --display-optimization-bailout",
30 | "build:clean": "rimraf ./build",
31 | "build:dll": "node ./internals/scripts/dependencies.js",
32 | "start": "cross-env NODE_ENV=development node server",
33 | "start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server",
34 | "start:production": "npm run test && npm run build && npm run start:prod",
35 | "start:prod": "cross-env NODE_ENV=production node server",
36 | "storybook": "start-storybook -p 9001 -c .storybook",
37 | "lint": "npm run lint:js",
38 | "lint:eslint": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts",
39 | "lint:js": "npm run lint:eslint -- . ",
40 | "lint:staged": "lint-staged",
41 | "pretest": "npm run test:clean && npm run lint",
42 | "test:clean": "rimraf ./coverage",
43 | "test": "cross-env NODE_ENV=test jest --no-cache --coverage",
44 | "test:watch": "cross-env NODE_ENV=test jest --watchAll",
45 | "coveralls": "cat ./coverage/lcov.info | coveralls"
46 | },
47 | "lint-staged": {
48 | "*.js": "lint:eslint"
49 | },
50 | "pre-commit": "lint:staged",
51 | "dllPlugin": {
52 | "path": "node_modules/react-boilerplate-dlls",
53 | "exclude": [
54 | "chalk",
55 | "compression",
56 | "cross-env",
57 | "express",
58 | "ip",
59 | "minimist",
60 | "sanitize.css"
61 | ],
62 | "include": [
63 | "core-js",
64 | "lodash",
65 | "eventsource-polyfill"
66 | ]
67 | },
68 | "dependencies": {
69 | "@material-ui/core": "^4.8.1",
70 | "@material-ui/icons": "^4.5.1",
71 | "babel-polyfill": "6.23.0",
72 | "compression": "1.6.2",
73 | "cross-env": "5.0.0",
74 | "dotenv-flow": "^3.1.0",
75 | "fontfaceobserver": "2.0.9",
76 | "formik": "^2.0.8",
77 | "formik-material-ui": "^1.0.0",
78 | "history": "^4.10.1",
79 | "hoist-non-react-statics": "2.1.1",
80 | "immutable": "3.8.1",
81 | "intl": "1.2.5",
82 | "invariant": "2.2.2",
83 | "ip": "1.1.5",
84 | "lodash": "4.17.11",
85 | "minimist": "1.2.0",
86 | "prop-types": "15.7.2",
87 | "react": "^16.12.0",
88 | "react-dom": "^16.12.0",
89 | "react-helmet": "5.2.0",
90 | "react-intl": "2.8.0",
91 | "react-redux": "^7.1.3",
92 | "react-router-dom": "^5.1.2",
93 | "redux": "^4.0.4",
94 | "redux-immutable": "4.0.0",
95 | "reselect": "4.0.0",
96 | "sanitize.css": "8.0.0",
97 | "styled-components": "4.2.0",
98 | "typeface-roboto": "0.0.54",
99 | "warning": "3.0.0",
100 | "whatwg-fetch": "3.0.0",
101 | "yup": "^0.28.0"
102 | },
103 | "devDependencies": {
104 | "@storybook/react": "^3.4.2",
105 | "add-asset-html-webpack-plugin": "2.0.1",
106 | "autoprefixer": "^9.5.0",
107 | "babel-cli": "6.26.0",
108 | "babel-core": "6.26.3",
109 | "babel-eslint": "7.2.3",
110 | "babel-loader": "7.1.0",
111 | "babel-plugin-dynamic-import-node": "1.0.2",
112 | "babel-plugin-react-intl": "3.0.1",
113 | "babel-plugin-react-transform": "2.0.2",
114 | "babel-plugin-styled-components": "1.10.0",
115 | "babel-plugin-transform-decorators-legacy": "^1.3.5",
116 | "babel-plugin-transform-es2015-modules-commonjs": "6.24.1",
117 | "babel-plugin-transform-react-constant-elements": "6.23.0",
118 | "babel-plugin-transform-react-inline-elements": "6.22.0",
119 | "babel-plugin-transform-react-remove-prop-types": "0.4.5",
120 | "babel-preset-env": "1.5.1",
121 | "babel-preset-react": "6.24.1",
122 | "babel-preset-stage-0": "6.24.1",
123 | "chalk": "2.4.2",
124 | "circular-dependency-plugin": "3.0.0",
125 | "coveralls": "2.13.1",
126 | "css-loader": "0.28.4",
127 | "cssnano": "^4.1.10",
128 | "enzyme": "^3.3.0",
129 | "enzyme-adapter-react-16": "^1.1.1",
130 | "enzyme-react-intl": "^2.0.4",
131 | "eslint": "3.19.0",
132 | "eslint-config-airbnb": "15.0.1",
133 | "eslint-config-airbnb-base": "11.2.0",
134 | "eslint-import-resolver-webpack": "0.8.3",
135 | "eslint-plugin-import": "2.7.0",
136 | "eslint-plugin-jsx-a11y": "5.0.3",
137 | "eslint-plugin-react": "7.0.1",
138 | "eventsource-polyfill": "0.9.6",
139 | "exports-loader": "0.6.4",
140 | "express": "4.15.3",
141 | "faker": "^4.1.0",
142 | "file-loader": "0.11.1",
143 | "html-loader": "0.4.5",
144 | "html-webpack-plugin": "2.29.0",
145 | "identity-obj-proxy": "^3.0.0",
146 | "image-webpack-loader": "2.0.0",
147 | "imports-loader": "0.7.1",
148 | "jest-cli": "20.0.4",
149 | "jest-localstorage-mock": "^2.2.0",
150 | "lint-staged": "3.5.1",
151 | "ngrok": "2.2.9",
152 | "node-plop": "0.7.0",
153 | "null-loader": "0.1.1",
154 | "offline-plugin": "4.8.1",
155 | "plop": "1.8.0",
156 | "postcss-loader": "^3.0.0",
157 | "pre-commit": "1.2.2",
158 | "precss": "^4.0.0",
159 | "react-test-renderer": "^16.4.1",
160 | "rimraf": "2.6.1",
161 | "shelljs": "0.7.7",
162 | "style-loader": "0.18.1",
163 | "url-loader": "0.5.8",
164 | "webpack": "3.5.5",
165 | "webpack-dev-middleware": "1.11.0",
166 | "webpack-hot-middleware": "2.18.0"
167 | }
168 | }
169 |
--------------------------------------------------------------------------------