├── .nvmrc
├── .gitattributes
├── .travis.yml
├── test
├── mocks
│ ├── store.js
│ ├── fakeSession.js
│ ├── fakeStorage.js
│ ├── reducers.js
│ └── i18n.js
├── setup.js
└── helpers
│ └── store.js
├── .stylelintrc
├── app
├── stylesheets
│ ├── base.css
│ ├── layout.css
│ ├── application.css
│ └── vars.css
├── locales
│ └── en
│ │ ├── modal.json
│ │ ├── common.json
│ │ ├── profile.json
│ │ ├── header.json
│ │ ├── about.json
│ │ ├── session.json
│ │ ├── todo.json
│ │ ├── home.json
│ │ └── article.json
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── services
│ ├── history.js
│ ├── sessionStorage.js
│ ├── currentUser.js
│ ├── i18n.js
│ └── api.js
├── components
│ ├── application
│ │ ├── styles.css
│ │ ├── tests
│ │ │ ├── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ │ └── index.snapshot.test.jsx
│ │ └── index.jsx
│ ├── navigation
│ │ ├── styles.css
│ │ ├── navItemLink.jsx
│ │ ├── tests
│ │ │ ├── index.snapshot.test.jsx
│ │ │ ├── index.test.jsx
│ │ │ └── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ └── index.jsx
│ ├── footer
│ │ ├── tests
│ │ │ ├── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ │ └── index.snapshot.test.jsx
│ │ ├── styles.css
│ │ └── index.jsx
│ ├── todo
│ │ ├── styles.css
│ │ ├── tests
│ │ │ ├── __snapshots__
│ │ │ │ ├── item.snapshot.test.jsx.snap
│ │ │ │ ├── list.snapshot.test.jsx.snap
│ │ │ │ ├── index.snapshot.test.jsx.snap
│ │ │ │ └── modal.snapshot.test.jsx.snap
│ │ │ ├── index.snapshot.test.jsx
│ │ │ ├── item.snapshot.test.jsx
│ │ │ ├── list.snapshot.test.jsx
│ │ │ ├── modal.snapshot.test.jsx
│ │ │ └── item.test.jsx
│ │ ├── list.jsx
│ │ ├── item.jsx
│ │ ├── index.jsx
│ │ └── modal.jsx
│ ├── home
│ │ ├── tests
│ │ │ ├── index.snapshot.test.jsx
│ │ │ └── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ └── index.jsx
│ ├── main
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── index.test.jsx
│ ├── profile
│ │ ├── tests
│ │ │ ├── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ │ └── index.snapshot.test.jsx
│ │ └── index.jsx
│ ├── about
│ │ ├── tests
│ │ │ ├── index.snapshot.test.jsx
│ │ │ └── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ └── index.jsx
│ ├── article
│ │ ├── index.jsx
│ │ └── tests
│ │ │ ├── index.snapshot.test.jsx
│ │ │ └── __snapshots__
│ │ │ └── index.snapshot.test.jsx.snap
│ ├── modal
│ │ ├── tests
│ │ │ ├── index.snapshot.test.jsx
│ │ │ └── __snapshots__
│ │ │ │ └── index.snapshot.test.jsx.snap
│ │ └── index.jsx
│ ├── form
│ │ ├── tests
│ │ │ └── index.test.jsx
│ │ └── index.jsx
│ ├── applicationRoutes
│ │ └── index.jsx
│ ├── signin
│ │ ├── tests
│ │ │ ├── modal.snapshot.test.jsx
│ │ │ └── __snapshots__
│ │ │ │ └── modal.snapshot.test.jsx.snap
│ │ └── modal.jsx
│ └── signup
│ │ ├── tests
│ │ ├── modal.snapshot.test.jsx
│ │ └── __snapshots__
│ │ │ └── modal.snapshot.test.jsx.snap
│ │ └── modal.jsx
├── containers
│ ├── profile
│ │ └── index.jsx
│ ├── main
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── index.test.js
│ ├── todoModal
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── index.test.js
│ ├── signinModal
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── index.test.js
│ ├── signupModal
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── index.test.js
│ ├── navigation
│ │ ├── index.jsx
│ │ └── tests
│ │ │ └── index.test.jsx
│ ├── todo
│ │ ├── tests
│ │ │ ├── index.test.js
│ │ │ └── list.test.js
│ │ ├── index.jsx
│ │ └── list.jsx
│ └── modal
│ │ ├── index.jsx
│ │ └── tests
│ │ └── index.test.js
├── sources
│ ├── users.js
│ ├── session.js
│ └── todos.js
├── actions
│ ├── modal.js
│ ├── todos.js
│ └── session.js
├── config
│ └── env
│ │ ├── test.js
│ │ ├── production.js
│ │ └── development.js
├── index.html
├── helpers
│ └── routes.js
├── reducers
│ ├── index.js
│ ├── modal
│ │ ├── index.js
│ │ └── index.test.js
│ ├── session
│ │ ├── index.js
│ │ └── index.test.js
│ └── todos
│ │ ├── index.js
│ │ └── index.test.js
├── stores
│ └── application.js
├── lib
│ └── storage.js
├── hoc
│ └── withAuth
│ │ ├── index.jsx
│ │ └── tests
│ │ └── index.test.jsx
└── index.jsx
├── .env.example
├── .gitignore
├── bin
└── setup
├── .babelrc
├── postcss.config.js
├── json-server
├── db
│ └── db.json.example
└── index.js
├── .eslintrc
├── .editorconfig
├── CONTRIBUTING.md
├── LICENSE
├── server.js
├── webpack.config.js
├── README.md
├── package.json
└── CHANGELOG.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 9
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | CHANGELOG merge=union
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "9"
4 |
--------------------------------------------------------------------------------
/test/mocks/store.js:
--------------------------------------------------------------------------------
1 | jest.mock('stores/application');
2 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-fs"
3 | }
4 |
--------------------------------------------------------------------------------
/app/stylesheets/base.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-size: 16px;
3 | }
4 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | PORT=8000
3 | NPM_CONFIG_PRODUCTION=false
4 |
--------------------------------------------------------------------------------
/app/stylesheets/layout.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | .app {
4 | height: 100%;
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/app/locales/en/modal.json:
--------------------------------------------------------------------------------
1 | {
2 | "signin": "Sign In",
3 | "signup": "Sign Up",
4 | "newTask": "New Task"
5 | }
6 |
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/react-base/HEAD/app/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/react-base/HEAD/app/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | node_modules
4 | coverage
5 | access_logs.db
6 | npm-debug.log
7 | dist
8 | json-server/db/db.json
9 |
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/react-base/HEAD/app/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/react-base/HEAD/app/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/app/services/history.js:
--------------------------------------------------------------------------------
1 | import createBrowserHistory from 'history/createBrowserHistory';
2 |
3 | export default createBrowserHistory();
4 |
--------------------------------------------------------------------------------
/app/components/application/styles.css:
--------------------------------------------------------------------------------
1 | .layout {
2 | height: 100%;
3 | }
4 |
5 | .wrapper {
6 | min-height: 100%;
7 | padding-bottom: 100px;
8 | }
9 |
--------------------------------------------------------------------------------
/app/locales/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "React-base",
3 | "errorNetwork": "Network error has occured. Please check internet connection"
4 | }
5 |
--------------------------------------------------------------------------------
/app/containers/profile/index.jsx:
--------------------------------------------------------------------------------
1 | import Profile from 'components/profile';
2 | import withAuth from 'hoc/withAuth';
3 |
4 | export default withAuth(Profile);
5 |
--------------------------------------------------------------------------------
/app/locales/en/profile.json:
--------------------------------------------------------------------------------
1 | {
2 | "text": "This page created to demonstrate ability to protect route. This page available only for authenticated users."
3 | }
--------------------------------------------------------------------------------
/app/locales/en/header.json:
--------------------------------------------------------------------------------
1 | {
2 | "home": "Home",
3 | "about": "About",
4 | "signIn": "Sign in",
5 | "signUp": "Sign up",
6 | "signOut": "Sign out"
7 | }
8 |
--------------------------------------------------------------------------------
/app/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | @import "bootstrap";
2 | @import "react-s-alert/dist/s-alert-default";
3 | @import "vars";
4 | @import "base";
5 | @import "layout";
6 |
--------------------------------------------------------------------------------
/app/stylesheets/vars.css:
--------------------------------------------------------------------------------
1 | $screen-xs: 480px;
2 | $screen-sm: 768px;
3 | $screen-md: 992px;
4 | $screen-lg: 1200px;
5 | $blue: #337ab7;
6 | $white: #fff;
7 | $grey-e7: #e7e7e7;
8 |
--------------------------------------------------------------------------------
/app/components/navigation/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../stylesheets/vars";
2 |
3 | .panel {
4 | border: none;
5 | border-bottom: 1px solid $grey-e7;
6 | border-radius: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/app/locales/en/about.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "About",
3 | "text": "Kick-start your new web application based on React and Redux technologies.",
4 | "details": "show details..."
5 | }
6 |
--------------------------------------------------------------------------------
/app/locales/en/session.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Name",
3 | "email": "Email",
4 | "password": "Password",
5 | "passwordConfirmation": "Password Confirmation",
6 | "submit": "Submit"
7 | }
8 |
--------------------------------------------------------------------------------
/app/locales/en/todo.json:
--------------------------------------------------------------------------------
1 | {
2 | "taskName": "Task name",
3 | "save": "Save",
4 | "list": "Todo List",
5 | "newTask": "New Task",
6 | "incomplete": "Incomplete",
7 | "complete": "Complete"
8 | }
9 |
--------------------------------------------------------------------------------
/app/locales/en/home.json:
--------------------------------------------------------------------------------
1 | {
2 | "text": "Kick-start your new web application based on React and Redux technologies. It also includes Webpack, React hot loader, PostCSS, JSON-server tools for even more rapid development."
3 | }
4 |
--------------------------------------------------------------------------------
/app/containers/main/index.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Main from 'components/main';
3 |
4 | const mapStateToProps = state => ({
5 | loggedIn: state.session.loggedIn,
6 | });
7 |
8 | export default connect(mapStateToProps)(Main);
9 |
--------------------------------------------------------------------------------
/app/sources/users.js:
--------------------------------------------------------------------------------
1 | import api from 'services/api';
2 |
3 | export default {
4 | urlRoot: '/users',
5 | async create(user) {
6 | return (
7 | await api.post(this.urlRoot, user, { withoutAuth: true })
8 | ).data;
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/app/actions/modal.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions';
2 |
3 | export const OPEN_MODAL = 'OPEN_MODAL';
4 | export const CLOSE_MODAL = 'CLOSE_MODAL';
5 |
6 | export const openModal = createAction(OPEN_MODAL);
7 | export const closeModal = createAction(CLOSE_MODAL);
8 |
--------------------------------------------------------------------------------
/app/containers/todoModal/index.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { createTodo } from 'actions/todos';
3 | import TodoModal from 'components/todo/modal';
4 |
5 | const mapDispatchToProps = {
6 | createTodo,
7 | };
8 |
9 | export default connect(null, mapDispatchToProps)(TodoModal);
10 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import 'jest-enzyme';
2 |
3 | import Enzyme from 'enzyme';
4 | import Adapter from 'enzyme-adapter-react-16';
5 |
6 | import './mocks/i18n';
7 | import './mocks/store';
8 | import './mocks/reducers';
9 |
10 | Enzyme.configure({ adapter: new Adapter() });
11 |
12 | global.context = describe;
13 |
--------------------------------------------------------------------------------
/app/components/footer/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Footer renders correctly 1`] = `
4 |
7 |
10 | React-base
11 |
12 |
13 | `;
14 |
--------------------------------------------------------------------------------
/app/components/footer/styles.css:
--------------------------------------------------------------------------------
1 | @import "../../stylesheets/vars";
2 |
3 | .footer {
4 | position: relative;
5 | height: 60px;
6 | margin-top: -60px;
7 | clear: both;
8 | border-top: 1px solid $grey-e7;
9 | line-height: 60px;
10 |
11 | p {
12 | margin: 0;
13 | text-align: center;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Install all dependencies
4 | yarn install
5 |
6 | # Setup configurable environment variables
7 | if [ ! -f .env ]; then
8 | cp .env.example .env
9 | fi
10 |
11 | # Setup db.json
12 | if [ ! -f json-server/db/db.json ]; then
13 | cp json-server/db/db.json.example json-server/db/db.json
14 | fi
15 |
--------------------------------------------------------------------------------
/app/components/footer/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import i18n from 'services/i18n';
3 | import styles from './styles.css';
4 |
5 | const Footer = () => (
6 |
9 | );
10 |
11 | export default Footer;
12 |
--------------------------------------------------------------------------------
/app/components/todo/styles.css:
--------------------------------------------------------------------------------
1 | .spacingTop {
2 | margin-top: 20px;
3 | }
4 |
5 | .iconTrash {
6 | margin-top: 2px;
7 | float: right;
8 | z-index: 10;
9 | }
10 |
11 | .todo {
12 | animation: fadeIn .5s;
13 | }
14 |
15 | @keyframes fadeIn {
16 | from {
17 | opacity: 0;
18 | }
19 |
20 | to {
21 | opacity: 1;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/config/env/test.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: process.env.NODE_ENV,
3 | target: 'http://localhost:8000',
4 | apiTarget: 'http://localhost:8000/v1',
5 | storage: {
6 | sessionKey: 'user_session',
7 | localizationKey: 'i18nextLng',
8 | },
9 | session: {
10 | tokenKey: 'authentication_token',
11 | emailKey: 'email',
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/app/sources/session.js:
--------------------------------------------------------------------------------
1 | import api from 'services/api';
2 |
3 | export default {
4 | urlRoot: '/session',
5 | async signin(user) {
6 | return (
7 | await api.post(this.urlRoot, user, { withoutAuth: true })
8 | ).data;
9 | },
10 | async logout({ id }) {
11 | return api.delete(`${this.urlRoot}/${id}`, { withoutAuth: true });
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/app/components/home/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Home from '../';
4 |
5 | describe('Home', () => {
6 | const renderComponent = () => renderer.create( );
7 |
8 | it('renders correctly', () => {
9 | expect(renderComponent().toJSON()).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/test/mocks/fakeSession.js:
--------------------------------------------------------------------------------
1 | export const fakeSession = {
2 | isLoading: false,
3 | loggedIn: true,
4 | currentUser: {
5 | email: 'test@test.com',
6 | password: 'password',
7 | id: 1,
8 | },
9 | };
10 |
11 | export const fakeEmptySession = {
12 | isLoading: false,
13 | loggedIn: false,
14 | currentUser: {},
15 | };
16 |
17 | export default fakeSession;
18 |
--------------------------------------------------------------------------------
/app/components/footer/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import Footer from '../';
4 |
5 | describe('Footer', () => {
6 | const renderComponent = () => renderer.create();
7 |
8 | it('renders correctly', () => {
9 | expect(renderComponent().toJSON()).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/app/components/main/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import TodoContainer from 'containers/todo';
4 | import Home from 'components/home';
5 |
6 | const Main = ({ loggedIn }) => (
7 | loggedIn ? :
8 | );
9 |
10 | Main.propTypes = {
11 | loggedIn: PropTypes.bool,
12 | };
13 |
14 | export default Main;
15 |
--------------------------------------------------------------------------------
/app/config/env/production.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: process.env.NODE_ENV,
3 | target: 'http://react-base.herokuapp.com',
4 | apiTarget: 'http://react-base-api.herokuapp.com',
5 | storage: {
6 | sessionKey: 'user_session',
7 | localizationKey: 'i18nextLng',
8 | },
9 | session: {
10 | tokenKey: 'authentication_token',
11 | emailKey: 'email',
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/app/config/env/development.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: process.env.NODE_ENV,
3 | target: 'http://localhost:8000',
4 | apiTarget: 'http://localhost:8000/v1',
5 | apiPath: '/v1',
6 | storage: {
7 | sessionKey: 'user_session',
8 | localizationKey: 'i18nextLng',
9 | },
10 | session: {
11 | tokenKey: 'authentication_token',
12 | emailKey: 'email',
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | React-base
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/containers/signinModal/index.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { signinUser } from 'actions/session';
3 | import SigninModal from 'components/signin/modal';
4 |
5 | const mapStateToProps = state => ({
6 | session: state.session,
7 | });
8 |
9 | const mapDispatchToProps = {
10 | signinUser,
11 | };
12 |
13 | export default connect(mapStateToProps, mapDispatchToProps)(SigninModal);
14 |
--------------------------------------------------------------------------------
/app/containers/signupModal/index.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { signupUser } from 'actions/session';
3 | import signupModal from 'components/signup/modal';
4 |
5 | const mapStateToProps = state => ({
6 | session: state.session,
7 | });
8 |
9 | const mapDispatchToProps = {
10 | signupUser,
11 | };
12 |
13 | export default connect(mapStateToProps, mapDispatchToProps)(signupModal);
14 |
--------------------------------------------------------------------------------
/app/components/home/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import i18n from 'services/i18n';
3 | import { Grid, Jumbotron } from 'react-bootstrap';
4 |
5 | const Home = () => (
6 |
7 |
8 | { i18n.t('common:projectName') }
9 |
10 |
11 |
12 | );
13 |
14 | export default Home;
15 |
--------------------------------------------------------------------------------
/test/mocks/fakeStorage.js:
--------------------------------------------------------------------------------
1 | const localStorageMock = (() => {
2 | let store = {};
3 |
4 | return {
5 | getItem(key) {
6 | return store[key] || null;
7 | },
8 | setItem(key, value) {
9 | store[key] = value.toString();
10 | },
11 | clear() {
12 | store = {};
13 | }
14 | };
15 | })();
16 |
17 | Object.defineProperty(window, 'localStorage', { value: localStorageMock });
18 |
--------------------------------------------------------------------------------
/app/components/todo/tests/__snapshots__/item.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TodoItem renders correctly 1`] = `
4 |
9 |
10 | Something to do
11 |
12 |
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/app/components/profile/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Profile renders correctly 1`] = `
4 |
9 |
10 | test@test.com
11 |
12 |
13 | This page created to demonstrate ability to protect route. This page available only for authenticated users.
14 |
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/app/helpers/routes.js:
--------------------------------------------------------------------------------
1 | import pathToRegexp from 'path-to-regexp';
2 |
3 | export const routes = {
4 | home: '/',
5 | about: '/about',
6 | profile: '/profile',
7 | signin: '/signin',
8 | signup: '/signup',
9 | aboutExtended: '/about/extended/:id',
10 | };
11 |
12 | export const paths = {};
13 |
14 | Object.keys(routes)
15 | .forEach((routeName) => {
16 | paths[routeName] = pathToRegexp.compile(routes[routeName]);
17 | });
18 |
19 | export default routes;
20 |
--------------------------------------------------------------------------------
/app/components/about/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import About from '../';
5 |
6 | describe('About', () => {
7 | const renderComponent = () => renderer.create((
8 |
9 |
10 |
11 | ));
12 |
13 | it('renders correctly', () => {
14 | expect(renderComponent().toJSON()).toMatchSnapshot();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | const getReducers = requireContext => (
4 | requireContext.keys().reduce((acc, value, key) => {
5 | if (key === 0) return acc;
6 |
7 | const domain = value.match(/^\.\/(.+?)\/.+/)[1];
8 |
9 | return {
10 | ...acc,
11 | [domain]: requireContext(value).default,
12 | };
13 | }, {})
14 | );
15 |
16 | export default combineReducers(getReducers(require.context('.', true, /.*^((?!\.test).)*\.js$/)));
17 |
--------------------------------------------------------------------------------
/app/stores/application.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import { composeWithDevTools } from 'redux-devtools-extension';
4 | import reducers from 'reducers';
5 |
6 | const store = createStore(
7 | reducers,
8 | composeWithDevTools(applyMiddleware(thunkMiddleware)),
9 | );
10 |
11 | if (module.hot) {
12 | module.hot.accept('reducers', () => {
13 | store.replaceReducer(reducers);
14 | });
15 | }
16 |
17 | export default store;
18 |
--------------------------------------------------------------------------------
/app/sources/todos.js:
--------------------------------------------------------------------------------
1 | import api from 'services/api';
2 |
3 | export default {
4 | urlRoot: '/todos',
5 | async get() {
6 | return (
7 | await api.get(this.urlRoot)
8 | ).data;
9 | },
10 | async create(todo) {
11 | return (
12 | await api.post(this.urlRoot, todo)
13 | ).data;
14 | },
15 | async update(todo) {
16 | return api.patch(`${this.urlRoot}/${todo.id}`, todo);
17 | },
18 | async delete({ id }) {
19 | return api.delete(`${this.urlRoot}/${id}`);
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/app/components/profile/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Grid } from 'react-bootstrap';
4 | import i18n from 'services/i18n';
5 |
6 | const Profile = ({ currentUser }) => (
7 |
8 | { currentUser.email }
9 | { i18n.t('profile:text') }
10 |
11 | );
12 |
13 | Profile.propTypes = {
14 | currentUser: PropTypes.shape({
15 | email: PropTypes.string.isRequired,
16 | }).isRequired,
17 | };
18 |
19 | export default Profile;
20 |
--------------------------------------------------------------------------------
/app/components/article/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import i18n from 'services/i18n';
4 |
5 | const Article = ({ match: { params } }) => (
6 |
7 | );
8 |
9 | Article.propTypes = {
10 | match: PropTypes.shape({
11 | params: PropTypes.shape({
12 | id: PropTypes.string.isRequired,
13 | }).isRequired,
14 | }).isRequired,
15 | };
16 |
17 | export default Article;
18 |
--------------------------------------------------------------------------------
/app/components/todo/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import toJson from 'enzyme-to-json';
4 | import Todo from '../';
5 |
6 | describe('Todo', () => {
7 | let props;
8 | const renderComponent = () => shallow( );
9 |
10 | beforeEach(() => {
11 | props = {
12 | openModal: () => {},
13 | };
14 | });
15 |
16 | it('renders correctly', () => {
17 | expect(toJson(renderComponent())).toMatchSnapshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/app/services/sessionStorage.js:
--------------------------------------------------------------------------------
1 | import storage from 'lib/storage';
2 | import config from 'config';
3 |
4 | export default class SessionStorage {
5 | static currentUser() {
6 | return storage.get(config.storage.sessionKey) || {};
7 | }
8 |
9 | static loggedIn() {
10 | return Object.keys(this.currentUser()).length !== 0;
11 | }
12 |
13 | static set(user) {
14 | storage.set(config.storage.sessionKey, user);
15 | }
16 |
17 | static remove() {
18 | storage.remove(config.storage.sessionKey);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/services/currentUser.js:
--------------------------------------------------------------------------------
1 | import store from 'stores/application';
2 | import config from 'config';
3 |
4 | export default class CurrentUser {
5 | static currentUser() {
6 | return store.getState().session.currentUser;
7 | }
8 |
9 | static loggedIn() {
10 | return !!Object.keys(this.currentUser()).length;
11 | }
12 |
13 | static get token() {
14 | return this.currentUser()[config.session.tokenKey];
15 | }
16 |
17 | static get email() {
18 | return this.currentUser()[config.session.emailKey];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/components/about/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`About renders correctly 1`] = `
4 |
7 |
8 |
9 | About
10 |
11 |
12 | Kick-start your new web application based on React and Redux technologies.
13 |
14 |
18 | show details...
19 |
20 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/app/components/application/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Application renders correctly 1`] = `
4 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/app/components/profile/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import toJson from 'enzyme-to-json';
4 | import { fakeSession } from 'mocks/fakeSession';
5 | import Profile from '../';
6 |
7 | describe('Profile', () => {
8 | let props;
9 | const renderComponent = () => shallow( );
10 |
11 | beforeEach(() => {
12 | props = fakeSession;
13 | });
14 |
15 | it('renders correctly', () => {
16 | expect(toJson(renderComponent())).toMatchSnapshot();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/app/components/modal/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import toJson from 'enzyme-to-json';
4 | import Modal from '../';
5 |
6 | describe('Modal', () => {
7 | let props;
8 | const renderComponent = () => shallow( );
9 |
10 | beforeEach(() => {
11 | props = {
12 | closeModal: () => {},
13 | isOpen: true,
14 | title: 'Test Modal',
15 | };
16 | });
17 |
18 | it('renders correctly', () => {
19 | expect(toJson(renderComponent())).toMatchSnapshot();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/app/reducers/modal/index.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import { OPEN_MODAL, CLOSE_MODAL } from 'actions/modal';
3 |
4 | const initialState = {
5 | isOpen: false,
6 | modalName: '',
7 | modalOptions: {},
8 | };
9 |
10 | export default handleActions({
11 | [OPEN_MODAL]: (state, { payload }) => {
12 | const { name, ...rest } = payload;
13 |
14 | return {
15 | isOpen: true,
16 | modalName: name,
17 | modalOptions: rest,
18 | };
19 | },
20 | [CLOSE_MODAL]: () => ({
21 | ...initialState,
22 | }),
23 | }, initialState);
24 |
--------------------------------------------------------------------------------
/app/components/form/tests/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Form from '../';
4 |
5 | describe('Form', () => {
6 | let props;
7 | let component;
8 | const renderComponent = () => shallow();
9 |
10 | beforeEach(() => {
11 | props = {
12 | onSubmit: jest.fn(),
13 | };
14 | });
15 |
16 | it('calls onSubmit callback', () => {
17 | component = renderComponent();
18 |
19 | component.find('form').simulate('submit');
20 |
21 | expect(props.onSubmit).toHaveBeenCalled();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/app/components/navigation/navItemLink.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink, Route } from 'react-router-dom';
3 | import { NavItem } from 'react-bootstrap';
4 |
5 | const NavItemLink = ({ to, exact, ...props }) => (
6 |
7 | {({ match }) => (
8 |
16 | )}
17 |
18 | );
19 |
20 | NavItemLink.propTypes = NavLink.propTypes;
21 |
22 | export default NavItemLink;
23 |
--------------------------------------------------------------------------------
/test/mocks/reducers.js:
--------------------------------------------------------------------------------
1 | jest.mock('reducers', () => {
2 | const { combineReducers } = require('redux');
3 | const REDUCERS_FOLDER = 'app/reducers';
4 |
5 | const getReducers = () => {
6 | const fs = require('fs');
7 |
8 | return fs.readdirSync(REDUCERS_FOLDER).reduce((acc, value, key) => {
9 | if (key === 0) return acc;
10 |
11 | const reducer = require(`../../${REDUCERS_FOLDER}/${value}/index.js`);
12 |
13 | return {
14 | ...acc,
15 | [value]: reducer.default
16 | };
17 | }, {});
18 | };
19 |
20 | return combineReducers(getReducers());
21 | });
22 |
--------------------------------------------------------------------------------
/test/helpers/store.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import configureStore from 'redux-mock-store';
3 | import thunk from 'redux-thunk';
4 |
5 | export function containerProps(component) {
6 | const componentProps = Object.keys(component.props());
7 | const rejectedProps = ['store', 'storeSubscription', 'dispatch'];
8 |
9 | return componentProps.filter(item => !rejectedProps.includes(item));
10 | }
11 |
12 | export function containerWithStore(Component, state = {}, props = {}) {
13 | const fakeStore = configureStore([thunk])(state);
14 |
15 | return ;
16 | }
17 |
--------------------------------------------------------------------------------
/app/lib/storage.js:
--------------------------------------------------------------------------------
1 | const STORAGE = localStorage;
2 |
3 | export default class Storage {
4 | static set(key, value) {
5 | const data = typeof value === 'object' ?
6 | JSON.stringify(value) :
7 | value;
8 |
9 | STORAGE.setItem(key, data);
10 | }
11 |
12 | static get(key) {
13 | const data = STORAGE.getItem(key);
14 |
15 | try {
16 | JSON.parse(data);
17 | } catch (e) {
18 | return data;
19 | }
20 |
21 | return JSON.parse(data);
22 | }
23 |
24 | static remove(key) {
25 | STORAGE.removeItem(key);
26 | }
27 |
28 | static clear() {
29 | STORAGE.clear();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/components/home/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Home renders correctly 1`] = `
4 |
7 |
10 |
11 | React-base
12 |
13 |
It also includes Webpack, React hot loader, PostCSS, JSON-server tools for even more rapid development.",
17 | }
18 | }
19 | />
20 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/app/containers/navigation/index.jsx:
--------------------------------------------------------------------------------
1 | import { withRouter } from 'react-router-dom';
2 | import { connect } from 'react-redux';
3 | import { logoutUser } from 'actions/session';
4 | import { openModal } from 'actions/modal';
5 | import Navigation from 'components/navigation';
6 |
7 | const mapStateToProps = state => state.session;
8 |
9 | const mapDispatchToProps = dispatch => ({
10 | logout: user => dispatch(logoutUser(user)),
11 | signin: () => dispatch(openModal({ name: 'signin' })),
12 | signup: () => dispatch(openModal({ name: 'signup' })),
13 | });
14 |
15 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Navigation));
16 |
--------------------------------------------------------------------------------
/app/components/todo/tests/item.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import TodoItem from '../item';
4 |
5 | describe('TodoItem', () => {
6 | let props;
7 | const renderComponent = () => renderer.create( );
8 |
9 | beforeEach(() => {
10 | props = {
11 | deleteTodo: () => {},
12 | todo: {
13 | id: 1,
14 | isComplete: false,
15 | name: 'Something to do',
16 | },
17 | updateTodo: () => {},
18 | };
19 | });
20 |
21 | it('renders correctly', () => {
22 | expect(renderComponent().toJSON()).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/app/components/application/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ApplicationRoutes from 'components/applicationRoutes';
3 | import Alert from 'react-s-alert';
4 | import Modal from 'containers/modal';
5 | import Navigation from 'containers/navigation';
6 | import Footer from 'components/footer';
7 | import styles from './styles.css';
8 |
9 | const Application = () => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default Application;
22 |
--------------------------------------------------------------------------------
/app/components/applicationRoutes/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route, Redirect } from 'react-router-dom';
3 | import Main from 'containers/main';
4 | import About from 'components/about';
5 | import Profile from 'containers/profile';
6 | import { routes } from 'helpers/routes';
7 |
8 | const ApplicationRoutes = () => (
9 |
10 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default ApplicationRoutes;
22 |
--------------------------------------------------------------------------------
/app/hoc/withAuth/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Redirect } from 'react-router-dom';
5 | import { paths } from 'helpers/routes';
6 |
7 | export default function withAuth(WrappedComponent) {
8 | const EnhancedComponent = (props) => {
9 | if (props.loggedIn) {
10 | return (
11 |
12 | );
13 | }
14 |
15 | return ;
16 | };
17 |
18 | EnhancedComponent.propTypes = {
19 | loggedIn: PropTypes.bool.isRequired,
20 | };
21 |
22 | return connect(state => state.session)(EnhancedComponent);
23 | }
24 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", { "modules": false }],
4 | "stage-0",
5 | "react"
6 | ],
7 | "plugins": [
8 | "react-hot-loader/babel"
9 | ],
10 | "env": {
11 | "production": {
12 | "plugins": [
13 | "transform-react-remove-prop-types",
14 | "transform-react-constant-elements",
15 | "transform-react-inline-elements"
16 | ]
17 | },
18 | "test": {
19 | "plugins": [
20 | "transform-es2015-modules-commonjs",
21 | ["module-resolver", {
22 | "root": ["./app/config/env"],
23 | "alias": {
24 | "config": "test"
25 | }
26 | }]
27 | ]
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/mocks/i18n.js:
--------------------------------------------------------------------------------
1 | jest.mock('services/i18n', () => {
2 | const i18n = require('i18next');
3 | const LOCALE_FOLDER = 'app/locales/en';
4 |
5 | const getResources = () => {
6 | const fs = require('fs');
7 |
8 | return fs.readdirSync(LOCALE_FOLDER).reduce((acc, value, key) => {
9 | const fileName = value.match(/(.+).json$/)[1];
10 |
11 | return {
12 | 'en': {
13 | ...acc['en'],
14 | [fileName]: require(`../../${LOCALE_FOLDER}/${value}`)
15 | }
16 | };
17 | }, {});
18 | };
19 |
20 | i18n.init({
21 | fallbackLng: 'en',
22 | interpolation: {
23 | escapeValue: false
24 | },
25 | resources: getResources()
26 | });
27 |
28 | return i18n;
29 | });
30 |
--------------------------------------------------------------------------------
/app/components/todo/tests/list.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import toJson from 'enzyme-to-json';
4 | import TodoList from '../list';
5 |
6 | describe('TodoList', () => {
7 | let props;
8 | const renderComponent = () => shallow( );
9 |
10 | beforeEach(() => {
11 | props = {
12 | deleteTodo: () => {},
13 | todos: [
14 | { id: 1, isComplete: false, name: 'Something to do 1' },
15 | { id: 2, isComplete: false, name: 'Something to do 2' },
16 | ],
17 | updateTodo: () => {},
18 | };
19 | });
20 |
21 | it('renders correctly', () => {
22 | expect(toJson(renderComponent())).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/app/containers/main/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import { fakeSession } from 'mocks/fakeSession';
4 | import Main from '../';
5 |
6 | describe('Main', () => {
7 | let state;
8 | let component;
9 | const renderComponent = () => shallow(containerWithStore(Main, state));
10 |
11 | beforeEach(() => {
12 | state = {
13 | session: fakeSession,
14 | };
15 | });
16 |
17 | it('renders Main component', () => {
18 | component = renderComponent().find('Main');
19 | const mainProps = containerProps(component);
20 |
21 | expect(component).toBePresent();
22 | expect(mainProps).toEqual(['loggedIn']);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/app/components/application/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import toJson from 'enzyme-to-json';
4 | import { fakeSession } from 'mocks/fakeSession';
5 | import Application from '../';
6 |
7 | describe('Application', () => {
8 | let props;
9 | const renderComponent = () => shallow((
10 |
11 |
12 |
13 | ));
14 |
15 | beforeEach(() => {
16 | props = {
17 | logout: () => {},
18 | session: fakeSession,
19 | signin: () => {},
20 | signup: () => {},
21 | };
22 | });
23 |
24 | it('renders correctly', () => {
25 | expect(toJson(renderComponent())).toMatchSnapshot();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/app/components/about/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Link } from 'react-router-dom';
3 | import { Grid } from 'react-bootstrap';
4 | import i18n from 'services/i18n';
5 | import { routes, paths } from 'helpers/routes';
6 | import Article from 'components/article';
7 |
8 | const About = () => {
9 | const id = 'test_id';
10 |
11 | return (
12 |
13 |
14 | {i18n.t('about:title')}
15 | {i18n.t('about:text')}
16 |
17 | {i18n.t('about:details')}
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default About;
26 |
--------------------------------------------------------------------------------
/app/components/todo/tests/__snapshots__/list.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TodoList renders correctly 1`] = `
4 |
7 |
19 |
31 |
32 | `;
33 |
--------------------------------------------------------------------------------
/app/components/form/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class Form extends Component {
5 | getChildContext = () => {
6 | const { errors } = this.props;
7 |
8 | return { errors };
9 | };
10 |
11 | render() {
12 | const { children, onSubmit } = this.props;
13 |
14 | return (
15 |
18 | );
19 | }
20 | }
21 |
22 | Form.propTypes = {
23 | children: PropTypes.node,
24 | errors: PropTypes.object,
25 | onSubmit: PropTypes.func.isRequired,
26 | };
27 |
28 | Form.childContextTypes = {
29 | errors: PropTypes.object,
30 | };
31 |
32 | Form.defaultProps = {
33 | errors: {},
34 | };
35 |
--------------------------------------------------------------------------------
/app/reducers/session/index.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import sessionStorage from 'services/sessionStorage';
3 | import { LOAD_DATA, SET_USER, REMOVE_USER } from 'actions/session';
4 |
5 | const initialState = {
6 | isLoading: false,
7 | loggedIn: sessionStorage.loggedIn(),
8 | currentUser: sessionStorage.currentUser(),
9 | };
10 |
11 | export default handleActions({
12 | [LOAD_DATA]: state => ({
13 | ...state,
14 | isLoading: true,
15 | }),
16 | [SET_USER]: (state, { payload }) => ({
17 | isLoading: false,
18 | loggedIn: true,
19 | currentUser: payload,
20 | }),
21 | [REMOVE_USER]: () => ({
22 | isLoading: false,
23 | loggedIn: false,
24 | currentUser: {},
25 | }),
26 | }, initialState);
27 |
--------------------------------------------------------------------------------
/app/components/article/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { MemoryRouter, Switch, Route } from 'react-router';
4 | import { routes, paths } from 'helpers/routes';
5 | import Article from '../';
6 |
7 | describe('Article', () => {
8 | const renderComponent = () => renderer.create((
9 |
10 |
11 |
16 |
17 |
18 | ));
19 |
20 | it('renders correctly', () => {
21 | expect(renderComponent().toJSON()).toMatchSnapshot();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/app/containers/todo/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import TodoContainer from '../';
4 |
5 | describe('TodoContainer', () => {
6 | let props;
7 | let component;
8 | const renderComponent = () => shallow(containerWithStore(TodoContainer, {}, props));
9 |
10 | beforeEach(() => {
11 | props = {
12 | fetchTodos: () => {},
13 | openModal: () => {},
14 | };
15 | });
16 |
17 | it('renders Todo component', () => {
18 | component = renderComponent().find('TodoContainer').shallow();
19 | const todoProps = containerProps(component);
20 |
21 | expect(component).toBePresent();
22 | expect(todoProps).toEqual(['openModal']);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const autoprefixer = require('autoprefixer');
2 | const postcssImport = require('postcss-import');
3 | const postcssMixins = require('postcss-mixins');
4 | const postcssExtend = require('postcss-extend');
5 | const postcssNested = require('postcss-nested');
6 | const postcssSimpleVars = require('postcss-simple-vars');
7 | const postcssColorFunction = require('postcss-color-function');
8 | const postcssPxtorem = require('postcss-pxtorem');
9 |
10 | module.exports = () => ({
11 | plugins: [
12 | postcssImport,
13 | postcssMixins,
14 | postcssNested,
15 | postcssExtend,
16 | postcssSimpleVars,
17 | postcssColorFunction,
18 | postcssPxtorem,
19 | autoprefixer({
20 | browsers: ['last 2 versions']
21 | })
22 | ]
23 | });
24 |
--------------------------------------------------------------------------------
/app/containers/todoModal/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import TodoModal from '../';
4 |
5 | describe('TodoModal', () => {
6 | let props;
7 | let component;
8 | const renderComponent = () => shallow(containerWithStore(TodoModal, {}, props));
9 |
10 | beforeEach(() => {
11 | props = {
12 | closeModal: () => {},
13 | isOpen: true,
14 | };
15 | });
16 |
17 | it('renders TodoModal component', () => {
18 | component = renderComponent().find('TodoModal');
19 | const todoModalProps = containerProps(component);
20 |
21 | expect(component).toBePresent();
22 | expect(todoModalProps).toEqual(['closeModal', 'isOpen', 'createTodo']);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/app/components/modal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Modal } from 'react-bootstrap';
4 |
5 | const ModalComponent = ({
6 | title, isOpen, closeModal, children,
7 | }) => (
8 |
13 |
14 |
15 | { title }
16 |
17 |
18 |
19 | { children }
20 |
21 |
22 | );
23 |
24 | ModalComponent.propTypes = {
25 | children: PropTypes.node,
26 | closeModal: PropTypes.func.isRequired,
27 | isOpen: PropTypes.bool.isRequired,
28 | title: PropTypes.string.isRequired,
29 | };
30 |
31 | export default ModalComponent;
32 |
--------------------------------------------------------------------------------
/json-server/db/db.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "todos": [
3 | {
4 | "id": 1,
5 | "name": "Pick up milk",
6 | "isComplete": false
7 | },
8 | {
9 | "id": 2,
10 | "name": "Pick up dry cleaning",
11 | "isComplete": true
12 | },
13 | {
14 | "id": 3,
15 | "name": "Grocery shopping",
16 | "isComplete": false
17 | },
18 | {
19 | "id": 4,
20 | "name": "Hem pants",
21 | "isComplete": false
22 | },
23 | {
24 | "id": 5,
25 | "name": "Oil change",
26 | "isComplete": true
27 | }
28 | ],
29 | "session": [
30 | {
31 | "id": 1,
32 | "name": "User",
33 | "email": "user@example.com",
34 | "authentication_token": "12937698127698712936"
35 | }
36 | ],
37 | "users": []
38 | }
39 |
--------------------------------------------------------------------------------
/app/components/todo/list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ListGroup } from 'react-bootstrap';
4 | import TodoItem from './item';
5 |
6 | const TodoList = ({ todos, updateTodo, deleteTodo }) => (
7 |
8 | {
9 | todos.map(todo => (
10 |
16 | ))
17 | }
18 |
19 | );
20 |
21 | TodoList.propTypes = {
22 | deleteTodo: PropTypes.func.isRequired,
23 | todos: PropTypes.arrayOf(PropTypes.shape({
24 | id: PropTypes.id,
25 | isComplete: PropTypes.bool,
26 | name: PropTypes.any,
27 | })).isRequired,
28 | updateTodo: PropTypes.func.isRequired,
29 | };
30 |
31 | export default TodoList;
32 |
--------------------------------------------------------------------------------
/app/containers/todo/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { fetchTodos } from 'actions/todos';
5 | import { openModal } from 'actions/modal';
6 | import Todo from 'components/todo';
7 |
8 | class TodoContainer extends Component {
9 | componentDidMount() {
10 | this.props.fetchTodos();
11 | }
12 |
13 | render() {
14 | return ;
15 | }
16 | }
17 |
18 | TodoContainer.propTypes = {
19 | fetchTodos: PropTypes.func.isRequired,
20 | openModal: PropTypes.func.isRequired,
21 | };
22 |
23 | const mapDispatchToProps = dispatch => ({
24 | fetchTodos: () => dispatch(fetchTodos()),
25 | openModal: () => dispatch(openModal({ name: 'todo' })),
26 | });
27 |
28 | export default connect(null, mapDispatchToProps)(TodoContainer);
29 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["airbnb", "plugin:jest/recommended"],
4 | "env": {
5 | "browser" : true,
6 | "jest": true
7 | },
8 | "rules": {
9 | "react/forbid-prop-types": 0,
10 | "react/require-default-props": 0,
11 | "jsx-a11y/label-has-for": 0,
12 | "jsx-a11y/anchor-is-valid": [ "error", {
13 | "components": ["Link"],
14 | "specialLink": ["to", "hrefLeft", "hrefRight"],
15 | "aspects": ["noHref", "invalidHref", "preferButton"]
16 | }]
17 | },
18 | "overrides": [
19 | {
20 | "files": [
21 | "app/**/*.test.js"
22 | ],
23 | "rules": {
24 | "max-statements": "off"
25 | }
26 | }
27 | ],
28 | "settings": {
29 | "import/resolver": {
30 | "webpack": {},
31 | "jest": {}
32 | }
33 | },
34 | "globals": {
35 | "context": false
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/index.jsx:
--------------------------------------------------------------------------------
1 | import 'stylesheets/application.css';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import { AppContainer } from 'react-hot-loader';
5 | import { Provider } from 'react-redux';
6 | import { Router } from 'react-router-dom';
7 | import store from 'stores/application';
8 | import appHistory from 'services/history';
9 | import Application from 'components/application';
10 |
11 | const renderComponent = (Component) => {
12 | render(
13 |
14 |
15 |
16 |
17 |
18 |
19 | ,
20 | document.getElementById('app'),
21 | );
22 | };
23 |
24 | renderComponent(Application);
25 |
26 | if (module.hot) {
27 | module.hot.accept('components/application', () => {
28 | renderComponent(Application);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | # A special property that should be specified at the top of the file outside of
4 | # any sections. Set to true to stop .editor config file search on current file
5 | root = true
6 |
7 | [*]
8 | # Indentation style
9 | # Possible values - tab, space
10 | indent_style = space
11 |
12 | # Indentation size in single-spaced characters
13 | # Possible values - an integer, tab
14 | indent_size = 2
15 |
16 | # Line ending file format
17 | # Possible values - lf, crlf, cr
18 | end_of_line = lf
19 |
20 | # File character encoding
21 | # Possible values - latin1, utf-8, utf-16be, utf-16le
22 | charset = utf-8
23 |
24 | # Denotes whether to trim whitespace at the end of lines
25 | # Possible values - true, false
26 | trim_trailing_whitespace = true
27 |
28 | # Denotes whether file should end with a newline
29 | # Possible values - true, false
30 | insert_final_newline = true
31 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 1. Fork the project.
4 | 2. Setup it on your machine with `bin/setup`.
5 | 3. Make sure the tests pass: `yarn test`.
6 | 4. Make your change. Add tests for your change when necessary. Make the tests pass: `yarn test`.
7 | 5. Mention your changes in "Unreleased" section of `CHANGELOG.md`
8 | 6. Push to your fork and [submit a pull request](https://help.github.com/articles/creating-a-pull-request/)
9 | (bonus points for topic branches).
10 |
11 | ## Releases
12 |
13 | We strictly follow [Semantic Versioning](http://semver.org/)
14 |
15 | 1. Make sure that tests are green.
16 | 2. Update project version in package.json
17 | 3. Update the changelog with the new version and commit it with message "release ".
18 | 4. Tag the release by running `git tag v`. Push the tag: `git push --tags`.
19 | 5. Verify that everything was pushed correctly on the Github: https://github.com/fs/react-base/releases
20 |
--------------------------------------------------------------------------------
/app/components/main/tests/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Main from '../';
4 |
5 | jest.mock('containers/todo', () => 'TodoContainer');
6 |
7 | describe('Main', () => {
8 | let props;
9 | let component;
10 | const renderComponent = () => shallow( );
11 |
12 | beforeEach(() => {
13 | props = {
14 | loggedIn: false,
15 | };
16 | });
17 |
18 | it('renders Home component', () => {
19 | component = renderComponent();
20 |
21 | expect(component.find('Home')).toBePresent();
22 | });
23 |
24 | context('when user is logged in', () => {
25 | beforeEach(() => {
26 | props = {
27 | loggedIn: true,
28 | };
29 | });
30 |
31 | it('renders TodoContainer component', () => {
32 | component = renderComponent();
33 |
34 | expect(component.find('TodoContainer')).toBePresent();
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/app/containers/signinModal/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import { fakeSession } from 'mocks/fakeSession';
4 | import SigninModal from '../';
5 |
6 | describe('SigninModal', () => {
7 | let props;
8 | let state;
9 | let component;
10 | const renderComponent = () => shallow(containerWithStore(SigninModal, state, props));
11 |
12 | beforeEach(() => {
13 | state = {
14 | session: fakeSession,
15 | };
16 | props = {
17 | closeModal: () => {},
18 | isOpen: true,
19 | };
20 | });
21 |
22 | it('renders SigninModal component', () => {
23 | component = renderComponent().find('SigninModal');
24 | const signinModalProps = containerProps(component);
25 |
26 | expect(component).toBePresent();
27 | expect(signinModalProps).toEqual(['closeModal', 'isOpen', 'session', 'signinUser']);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/containers/signupModal/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import { fakeSession } from 'mocks/fakeSession';
4 | import SignupModal from '../';
5 |
6 | describe('SignupModal', () => {
7 | let props;
8 | let state;
9 | let component;
10 | const renderComponent = () => shallow(containerWithStore(SignupModal, state, props));
11 |
12 | beforeEach(() => {
13 | state = {
14 | session: fakeSession,
15 | };
16 | props = {
17 | closeModal: () => { },
18 | isOpen: true,
19 | };
20 | });
21 |
22 | it('renders SignupModal component', () => {
23 | component = renderComponent().find('SignupModal');
24 | const signupModalProps = containerProps(component);
25 |
26 | expect(component).toBePresent();
27 | expect(signupModalProps).toEqual(['closeModal', 'isOpen', 'session', 'signupUser']);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/json-server/index.js:
--------------------------------------------------------------------------------
1 | const jsonServer = require('json-server');
2 | const { apiPath } = require('../app/config/env/development');
3 |
4 | module.exports = {
5 | initialize(server) {
6 | const router = jsonServer.router('./json-server/db/db.json');
7 |
8 | server.use(jsonServer.defaults());
9 | server.use(apiPath, router);
10 |
11 | // Has overrided json-server render method to simulate server side error response.
12 | // Error will be returned if you try to sign in with error@example.com email
13 | router.render = (
14 | {
15 | url,
16 | method,
17 | body: { email }
18 | },
19 | res
20 | ) => {
21 | if (
22 | url === '/session' &&
23 | method === 'POST' &&
24 | email === 'error@example.com'
25 | ) {
26 | res.status(500).jsonp({ error: 'Server error has occured' });
27 | } else {
28 | res.jsonp(res.locals.data);
29 | }
30 | };
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/components/todo/tests/modal.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import TodoModal from '../modal';
4 |
5 | jest.mock('components/modal', () => 'Modal');
6 |
7 | describe('TodoModal', () => {
8 | let props;
9 | let component;
10 | const renderComponent = () => renderer.create( );
11 |
12 | beforeEach(() => {
13 | props = {
14 | closeModal: () => { },
15 | createTodo: () => { },
16 | isOpen: true,
17 | };
18 | });
19 |
20 | it('renders correctly', () => {
21 | expect(renderComponent().toJSON()).toMatchSnapshot();
22 | });
23 |
24 | context('when form is invalid', () => {
25 | beforeEach(() => {
26 | component = renderComponent();
27 |
28 | component.getInstance().setState({ email: 'qwe' });
29 | });
30 |
31 | it('renders form with validation errors', () => {
32 | expect(component.toJSON()).toMatchSnapshot();
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/app/reducers/todos/index.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions';
2 | import {
3 | LOAD_TODOS,
4 | SET_TODOS,
5 | ADD_TODO,
6 | TOGGLE_TODO,
7 | REMOVE_TODO,
8 | } from 'actions/todos';
9 |
10 | const initialState = {
11 | isLoading: false,
12 | todos: [],
13 | };
14 |
15 | export default handleActions({
16 | [LOAD_TODOS]: state => ({
17 | ...state,
18 | isLoading: true,
19 | }),
20 | [SET_TODOS]: (state, { payload }) => ({
21 | ...state,
22 | isLoading: false,
23 | todos: payload,
24 | }),
25 | [ADD_TODO]: (state, { payload }) => ({
26 | ...state,
27 | todos: [...state.todos, payload],
28 | }),
29 | [TOGGLE_TODO]: (state, { payload }) => ({
30 | ...state,
31 | todos: state.todos.map(todo => (
32 | todo.id === payload.id ? payload : todo
33 | )),
34 | }),
35 | [REMOVE_TODO]: (state, { payload }) => ({
36 | ...state,
37 | todos: state.todos.filter(todo => todo.id !== payload.id),
38 | }),
39 | }, initialState);
40 |
--------------------------------------------------------------------------------
/app/containers/navigation/tests/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { MemoryRouter } from 'react-router';
4 | import { containerWithStore, containerProps } from 'helpers/store';
5 | import { fakeSession } from 'mocks/fakeSession';
6 | import Navigation from '../';
7 |
8 | describe('Navigation', () => {
9 | let state;
10 | let component;
11 | const renderComponent = () => mount((
12 |
13 | {containerWithStore(Navigation, state)}
14 |
15 | ));
16 |
17 | beforeEach(() => {
18 | state = {
19 | session: fakeSession,
20 | };
21 | });
22 |
23 | it('renders Navigation component', () => {
24 | component = renderComponent().find('Navigation');
25 | const navigationProps = containerProps(component);
26 |
27 | expect(component).toBePresent();
28 | expect(navigationProps).toEqual(expect.arrayContaining(['loggedIn', 'currentUser', 'logout', 'signin', 'signup']));
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/app/containers/todo/list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import {
5 | deleteTodo as deleteTodoAction,
6 | updateTodo as updateTodoAction,
7 | } from 'actions/todos';
8 | import TodoList from 'components/todo/list';
9 |
10 | const TodoListContainer = ({
11 | isComplete, todos, updateTodo, deleteTodo,
12 | }) => (
13 | todo.isComplete === isComplete)}
15 | deleteTodo={deleteTodo}
16 | updateTodo={updateTodo}
17 | />
18 | );
19 |
20 | TodoListContainer.propTypes = {
21 | deleteTodo: PropTypes.func.isRequired,
22 | isComplete: PropTypes.bool.isRequired,
23 | todos: PropTypes.array.isRequired,
24 | updateTodo: PropTypes.func.isRequired,
25 | };
26 |
27 | const mapStateToProps = state => ({
28 | ...state.todos,
29 | });
30 |
31 | const mapDispatchToProps = {
32 | deleteTodo: deleteTodoAction,
33 | updateTodo: updateTodoAction,
34 | };
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps)(TodoListContainer);
37 |
--------------------------------------------------------------------------------
/app/components/navigation/tests/index.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import { fakeSession, fakeEmptySession } from 'mocks/fakeSession';
5 | import Navigation from 'components/navigation';
6 |
7 | describe('Navigation', () => {
8 | let props;
9 | const renderComponent = () => renderer.create((
10 |
11 |
12 |
13 | ));
14 |
15 | beforeEach(() => {
16 | props = {
17 | ...fakeEmptySession,
18 | logout: () => {},
19 | signin: () => {},
20 | signup: () => {},
21 | };
22 | });
23 |
24 | it('renders correctly', () => {
25 | expect(renderComponent().toJSON()).toMatchSnapshot();
26 | });
27 |
28 | context('when user is logged in', () => {
29 | beforeEach(() => {
30 | props = {
31 | ...props,
32 | ...fakeSession,
33 | };
34 | });
35 |
36 | it('renders user navigations', () => {
37 | expect(renderComponent().toJSON()).toMatchSnapshot();
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/app/components/signin/tests/modal.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { fakeSession } from 'mocks/fakeSession';
4 | import SigninModal from '../modal';
5 |
6 | jest.mock('components/modal', () => 'Modal');
7 |
8 | describe('SigninModal', () => {
9 | let props;
10 | let component;
11 | const renderComponent = () => renderer.create( );
12 |
13 | beforeEach(() => {
14 | props = {
15 | closeModal: () => {},
16 | isOpen: true,
17 | session: fakeSession,
18 | signinUser: () => {},
19 | };
20 | });
21 |
22 | it('renders correctly', () => {
23 | expect(renderComponent().toJSON()).toMatchSnapshot();
24 | });
25 |
26 | context('when form is invalid', () => {
27 | beforeEach(() => {
28 | component = renderComponent();
29 |
30 | component.getInstance().setState({
31 | email: 'qwe',
32 | password: 'asd',
33 | });
34 | });
35 |
36 | it('renders form with validation errors', () => {
37 | expect(component.toJSON()).toMatchSnapshot();
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/app/components/modal/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Modal renders correctly 1`] = `
4 |
30 |
35 |
38 | Test Modal
39 |
40 |
41 |
45 |
46 | `;
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2018 Flatstack
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/app/containers/modal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { closeModal as closeModalAction } from 'actions/modal';
5 | import TodoModal from 'containers/todoModal';
6 | import SigninModal from 'containers/signinModal';
7 | import SignupModal from 'containers/signupModal';
8 |
9 | const MODALS = {
10 | todo: TodoModal,
11 | signin: SigninModal,
12 | signup: SignupModal,
13 | };
14 |
15 | const ModalContainer = ({ modal, closeModal }) => {
16 | const { modalName, ...rest } = modal;
17 |
18 | if (modalName) {
19 | const params = { ...rest, closeModal };
20 | const CurrentModal = MODALS[modalName];
21 |
22 | return ;
23 | }
24 |
25 | return null;
26 | };
27 |
28 | ModalContainer.propTypes = {
29 | closeModal: PropTypes.func.isRequired,
30 | modal: PropTypes.object.isRequired,
31 | };
32 |
33 | const mapStateToProps = state => ({
34 | modal: state.modal,
35 | });
36 |
37 | const mapDispatchToProps = {
38 | closeModal: closeModalAction,
39 | };
40 |
41 | export default connect(mapStateToProps, mapDispatchToProps)(ModalContainer);
42 |
--------------------------------------------------------------------------------
/app/components/signup/tests/modal.snapshot.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { fakeSession } from 'mocks/fakeSession';
4 | import SignupModal from '../modal';
5 |
6 | jest.mock('components/modal', () => 'Modal');
7 |
8 | describe('SignupModal', () => {
9 | let props;
10 | let component;
11 | const renderComponent = () => renderer.create( );
12 |
13 | beforeEach(() => {
14 | props = {
15 | closeModal: () => { },
16 | isOpen: true,
17 | session: fakeSession,
18 | signupUser: () => { },
19 | };
20 | });
21 |
22 | it('renders correctly', () => {
23 | expect(renderComponent().toJSON()).toMatchSnapshot();
24 | });
25 |
26 | context('when form is invalid', () => {
27 | beforeEach(() => {
28 | component = renderComponent();
29 |
30 | component.getInstance().setState({
31 | name: 'qwe',
32 | email: 'qwe',
33 | password: 'asd',
34 | passwordConfirmation: 'asd',
35 | });
36 | });
37 |
38 | it('renders form with validation errors', () => {
39 | expect(component.toJSON()).toMatchSnapshot();
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/app/locales/en/article.json:
--------------------------------------------------------------------------------
1 | {
2 | "text": " Why React? Edit on GitHub React is a JavaScript library for creating user interfaces by Facebook and Instagram. Many people choose to think of React as the V in MVC. We built React to solve one problem: building large applications with data that changes over time.
Simple Simply express how your app should look at any given point in time, and React will automatically manage all UI updates when your underlying data changes.
Declarative When the data changes, React conceptually hits the \"refresh\" button, and knows to only update the changed parts.
Build Composable Components React is all about building reusable components. In fact, with React the only thing you do is build components. Since they're so encapsulated, components make code reuse, testing, and separation of concerns easy.
Give It Five Minutes React challenges a lot of conventional wisdom, and at first glance some of the ideas may seem crazy. Give it five minutes while reading this guide; those crazy ideas have worked for building thousands of components both inside and outside of Facebook and Instagram.
article id: {{articleId}} "
3 | }
4 |
--------------------------------------------------------------------------------
/app/services/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import languageDetector from 'i18next-browser-languagedetector';
3 | import storage from 'lib/storage';
4 | import config from 'config';
5 |
6 | const { localizationKey } = config.storage;
7 | const defaultLanguage = 'en';
8 | const currentLanguage = storage.get(localizationKey);
9 | const getResources = () => {
10 | const requireContext = require.context('../locales', true, /\.json$/);
11 |
12 | return requireContext.keys().reduce((acc, value) => {
13 | const fileName = value.match(/.+\/(.+).json$/)[1];
14 | const languageDomain = value.match(/^\.\/(.+?)\/.+/)[1];
15 |
16 | return {
17 | ...acc,
18 | [languageDomain]: {
19 | ...acc[languageDomain],
20 | [fileName]: requireContext(value),
21 | },
22 | };
23 | }, {});
24 | };
25 |
26 | i18n
27 | .use(languageDetector)
28 | .init({
29 | fallbackLng: defaultLanguage,
30 |
31 | interpolation: {
32 | escapeValue: false,
33 | },
34 |
35 | lookupLocalStorage: localizationKey,
36 | caches: ['localStorage'],
37 |
38 | resources: getResources(),
39 | });
40 |
41 | if (!currentLanguage) {
42 | i18n.changeLanguage(defaultLanguage);
43 | }
44 |
45 | export default i18n;
46 |
--------------------------------------------------------------------------------
/app/components/todo/item.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import { ListGroupItem } from 'react-bootstrap';
5 | import styles from './styles.css';
6 |
7 | const TodoItem = ({ todo, updateTodo, deleteTodo }) => {
8 | const toggle = () => {
9 | updateTodo({
10 | ...todo,
11 | isComplete: !todo.isComplete,
12 | });
13 | };
14 |
15 | const remove = (event) => {
16 | deleteTodo(todo);
17 | event.stopPropagation();
18 | };
19 |
20 | return (
21 |
25 |
26 | { todo.name }
27 |
28 |
32 |
33 | );
34 | };
35 |
36 | TodoItem.propTypes = {
37 | deleteTodo: PropTypes.func.isRequired,
38 | todo: PropTypes.shape({
39 | id: PropTypes.number,
40 | isComplete: PropTypes.bool,
41 | name: PropTypes.any,
42 | }).isRequired,
43 | updateTodo: PropTypes.func.isRequired,
44 | };
45 |
46 | export default TodoItem;
47 |
--------------------------------------------------------------------------------
/app/actions/todos.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions';
2 | import todosSource from 'sources/todos';
3 |
4 | export const LOAD_TODOS = 'LOAD_TODOS';
5 | export const SET_TODOS = 'SET_TODOS';
6 | export const ADD_TODO = 'ADD_TODO';
7 | export const TOGGLE_TODO = 'TOGGLE_TODO';
8 | export const REMOVE_TODO = 'REMOVE_TODO';
9 |
10 | export const loadTodos = createAction(LOAD_TODOS);
11 | export const setTodos = createAction(SET_TODOS);
12 | export const addTodo = createAction(ADD_TODO);
13 | export const toggleTodo = createAction(TOGGLE_TODO);
14 | export const removeTodo = createAction(REMOVE_TODO);
15 |
16 | export const fetchTodos = () =>
17 | async (dispatch) => {
18 | dispatch(loadTodos());
19 |
20 | const result = await todosSource.get();
21 |
22 | dispatch(setTodos(result));
23 | };
24 |
25 | export const createTodo = todo =>
26 | async (dispatch) => {
27 | const result = await todosSource.create({ ...todo, isComplete: false });
28 |
29 | dispatch(addTodo(result));
30 | };
31 |
32 | export const updateTodo = todo =>
33 | (dispatch) => {
34 | dispatch(toggleTodo(todo));
35 | todosSource.update(todo);
36 | };
37 |
38 | export const deleteTodo = todo =>
39 | (dispatch) => {
40 | dispatch(removeTodo(todo));
41 | todosSource.delete(todo);
42 | };
43 |
--------------------------------------------------------------------------------
/app/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Alert from 'react-s-alert';
3 | import currentUser from 'services/currentUser';
4 | import sessionStorage from 'services/sessionStorage';
5 | import i18n from 'services/i18n';
6 | import config from 'config';
7 |
8 | const api = axios.create({ baseURL: config.apiTarget });
9 |
10 | api.interceptors.request.use(
11 | (axiosConfig) => {
12 | if (axiosConfig.withoutAuth) return axiosConfig;
13 |
14 | return {
15 | ...axiosConfig,
16 | headers: {
17 | ...axiosConfig.headers,
18 | 'X-User-Email': currentUser.email,
19 | 'X-User-Token': currentUser.token,
20 | },
21 | };
22 | },
23 | error => Promise.reject(error),
24 | );
25 |
26 |
27 | api.interceptors.response.use(
28 | response => response,
29 | (error) => {
30 | const {
31 | message,
32 | response: errorResponse,
33 | } = error;
34 |
35 | if (errorResponse) {
36 | if (errorResponse.status === 401) {
37 | sessionStorage.remove();
38 | } else {
39 | Alert.error(errorResponse.data.error);
40 | }
41 | } else if (message === 'Network Error') {
42 | Alert.error(i18n.t('common:errorNetwork'));
43 | }
44 |
45 | return Promise.reject(error);
46 | },
47 | );
48 |
49 | export default api;
50 |
--------------------------------------------------------------------------------
/app/components/todo/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | Grid,
5 | Row,
6 | Col,
7 | Button,
8 | } from 'react-bootstrap';
9 | import i18n from 'services/i18n';
10 | import TodoList from 'containers/todo/list';
11 | import styles from './styles.css';
12 |
13 | const Todo = props => (
14 |
15 |
16 |
17 | { i18n.t('todo:list') }
18 |
19 |
20 |
25 | { i18n.t('todo:newTask') }
26 |
27 |
28 |
29 |
30 |
31 |
32 | { i18n.t('todo:incomplete') }
33 |
34 |
35 |
36 |
37 |
38 | { i18n.t('todo:complete') }
39 |
40 |
41 |
42 |
43 |
44 | );
45 |
46 | Todo.propTypes = {
47 | openModal: PropTypes.func.isRequired,
48 | };
49 |
50 | export default Todo;
51 |
--------------------------------------------------------------------------------
/app/actions/session.js:
--------------------------------------------------------------------------------
1 | import { createAction } from 'redux-actions';
2 | import sessionSource from 'sources/session';
3 | import usersSource from 'sources/users';
4 | import sessionStorage from 'services/sessionStorage';
5 | import appHistory from 'services/history';
6 | import { paths } from 'helpers/routes';
7 |
8 | export const LOAD_DATA = 'LOAD_DATA';
9 | export const SET_USER = 'SET_USER';
10 | export const REMOVE_USER = 'REMOVE_USER';
11 |
12 | export const loadData = createAction(LOAD_DATA);
13 | export const setUser = createAction(SET_USER);
14 | export const removeUser = createAction(REMOVE_USER);
15 |
16 | export const signinUser = user =>
17 | async (dispatch) => {
18 | dispatch(loadData());
19 |
20 | const result = await sessionSource.signin(user);
21 |
22 | sessionStorage.set(result);
23 | dispatch(setUser(result));
24 | appHistory.push(paths.home());
25 | };
26 |
27 | export const signupUser = user =>
28 | async (dispatch) => {
29 | dispatch(loadData());
30 |
31 | const result = await usersSource.create(user);
32 |
33 | sessionStorage.set(result);
34 | dispatch(setUser(result));
35 | appHistory.push(paths.home());
36 | };
37 |
38 | export const logoutUser = user =>
39 | async (dispatch) => {
40 | await sessionSource.logout(user);
41 | sessionStorage.remove();
42 | dispatch(removeUser());
43 | };
44 |
--------------------------------------------------------------------------------
/app/components/todo/tests/item.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import TodoItem from '../item';
4 |
5 | describe('TodoItem', () => {
6 | let props;
7 | let component;
8 | const renderComponent = () => mount( );
9 |
10 | describe('callbacks', () => {
11 | const deleteTodo = jest.fn();
12 | const updateTodo = jest.fn();
13 |
14 | beforeEach(() => {
15 | props = {
16 | deleteTodo,
17 | updateTodo,
18 | todo: {
19 | id: 1,
20 | isComplete: false,
21 | name: 'Some Todo',
22 | },
23 | };
24 | });
25 |
26 | it('calls .updateTodo() when clicking on li and mark todo as complete', () => {
27 | component = renderComponent();
28 | component.find('ListGroupItem').simulate('click');
29 |
30 | expect(updateTodo).toHaveBeenCalledWith({
31 | id: 1,
32 | isComplete: true,
33 | name: 'Some Todo',
34 | });
35 | });
36 |
37 | it('calls .deleteTodo() when clicking on delete icon', () => {
38 | component = renderComponent();
39 | component.find('.glyphicon-trash').simulate('click');
40 |
41 | expect(deleteTodo).toHaveBeenCalledWith({
42 | id: 1,
43 | isComplete: false,
44 | name: 'Some Todo',
45 | });
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/app/components/article/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Article renders correctly 1`] = `
4 | Why React? Edit on GitHub React is a JavaScript library for creating user interfaces by Facebook and Instagram. Many people choose to think of React as the V in MVC. We built React to solve one problem: building large applications with data that changes over time.
Simple Simply express how your app should look at any given point in time, and React will automatically manage all UI updates when your underlying data changes.
Declarative When the data changes, React conceptually hits the \\"refresh\\" button, and knows to only update the changed parts.
Build Composable Components React is all about building reusable components. In fact, with React the only thing you do is build components. Since they're so encapsulated, components make code reuse, testing, and separation of concerns easy.
Give It Five Minutes React challenges a lot of conventional wisdom, and at first glance some of the ideas may seem crazy. Give it five minutes while reading this guide; those crazy ideas have worked for building thousands of components both inside and outside of Facebook and Instagram.
article id: 1 ",
8 | }
9 | }
10 | />
11 | `;
12 |
--------------------------------------------------------------------------------
/app/containers/todo/tests/list.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import TodoListContainer from '../list';
4 |
5 | describe('TodoListContainer', () => {
6 | let state;
7 | let props;
8 | let component;
9 | const renderComponent = () => shallow(containerWithStore(TodoListContainer, state, props));
10 |
11 | beforeEach(() => {
12 | state = {
13 | todos: {
14 | isLoading: false,
15 | todos: [
16 | { id: 1, isComplete: true, name: 'Something to do 1' },
17 | { id: 2, isComplete: false, name: 'Something to do 2' },
18 | { id: 3, isComplete: false, name: 'Something to do 3' },
19 | ],
20 | },
21 | };
22 | props = {
23 | deleteTodo: () => {},
24 | updateTodo: () => {},
25 | isComplete: true,
26 | };
27 | });
28 |
29 | it('renders TodoList component', () => {
30 | component = renderComponent().find('TodoListContainer').shallow();
31 | const todoListProps = containerProps(component);
32 |
33 | expect(component).toBePresent();
34 | expect(todoListProps).toEqual(['todos', 'deleteTodo', 'updateTodo']);
35 | });
36 |
37 | it('filters todos by isComplete', () => {
38 | component = renderComponent().find('TodoListContainer').shallow();
39 | const todos = component.prop('todos');
40 |
41 | expect(todos).toHaveLength(1);
42 | expect(todos).toContainEqual({ id: 1, isComplete: true, name: 'Something to do 1' });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/app/components/todo/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Todo renders correctly 1`] = `
4 |
9 |
14 |
19 |
20 | Todo List
21 |
22 |
23 |
28 |
37 | New Task
38 |
39 |
40 |
41 |
46 |
51 |
54 | Incomplete
55 |
56 |
59 |
60 |
65 |
68 | Complete
69 |
70 |
73 |
74 |
75 |
76 | `;
77 |
--------------------------------------------------------------------------------
/app/reducers/modal/index.test.js:
--------------------------------------------------------------------------------
1 | import reducer from './';
2 |
3 | describe('Modal reducer', () => {
4 | let initialState;
5 | let state;
6 | let actionType;
7 | let payload;
8 |
9 | const callReducer = () => reducer(state, { type: actionType, payload });
10 |
11 | beforeEach(() => {
12 | initialState = {
13 | isOpen: false,
14 | modalName: '',
15 | modalOptions: {},
16 | };
17 | });
18 |
19 | it('returns initial state', () => {
20 | expect(callReducer()).toEqual(initialState);
21 | });
22 |
23 | context('when state is present', () => {
24 | beforeEach(() => {
25 | state = initialState;
26 |
27 | payload = {
28 | name: 'Awesome Modal',
29 | someOption: 'Some option value',
30 | };
31 | });
32 |
33 | describe('OPEN_MODAL', () => {
34 | beforeEach(() => {
35 | actionType = 'OPEN_MODAL';
36 | });
37 |
38 | it('returns new state', () => {
39 | expect(callReducer()).toEqual({
40 | isOpen: true,
41 | modalName: 'Awesome Modal',
42 | modalOptions: {
43 | someOption: 'Some option value',
44 | },
45 | });
46 | });
47 | });
48 |
49 | describe('CLOSE_MODAL', () => {
50 | beforeEach(() => {
51 | actionType = 'CLOSE_MODAL';
52 |
53 | state = {
54 | isOpen: true,
55 | modalName: 'Awesome Modal',
56 | modalOptions: {
57 | someOption: 'Some option value',
58 | },
59 | };
60 | });
61 |
62 | it('returns initial state', () => {
63 | expect(callReducer()).toEqual(initialState);
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/app/hoc/withAuth/tests/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { MemoryRouter, Switch, Route } from 'react-router';
4 | import { containerWithStore } from 'helpers/store';
5 | import { fakeSession, fakeEmptySession } from 'mocks/fakeSession';
6 | import { paths } from 'helpers/routes';
7 | import withAuth from '../';
8 |
9 | describe('withAuth HOC', () => {
10 | let state;
11 |
12 | const WrappedComponent = () => null;
13 | const Home = () => null;
14 |
15 | const renderComponent = () => {
16 | const EnhancedComponent = () => containerWithStore(withAuth(WrappedComponent), state);
17 |
18 | return mount((
19 |
20 |
21 |
26 |
27 |
28 |
29 | ));
30 | };
31 |
32 | context('when authenticated', () => {
33 | beforeEach(() => {
34 | state = {
35 | session: fakeSession,
36 | };
37 | });
38 |
39 | it('renders WrappedComponent', () => {
40 | const component = renderComponent().find(WrappedComponent);
41 |
42 | expect(component).toBePresent();
43 | });
44 | });
45 |
46 | context('when unauthenticated', () => {
47 | beforeEach(() => {
48 | state = {
49 | session: fakeEmptySession,
50 | };
51 | });
52 |
53 | it('redirect to root', () => {
54 | const component = renderComponent().find(Home);
55 |
56 | expect(component).toBePresent();
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/app/components/navigation/tests/index.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { MemoryRouter } from 'react-router-dom';
4 | import Navigation from 'components/navigation';
5 |
6 | describe('Navigation', () => {
7 | const signup = jest.fn();
8 | const signin = jest.fn();
9 | const logout = jest.fn();
10 | let props;
11 | const renderComponent = () => shallow((
12 |
13 |
14 |
15 | ))
16 | .find(Navigation)
17 | .dive();
18 |
19 | beforeEach(() => {
20 | props = {
21 | signin,
22 | signup,
23 | logout,
24 | loggedIn: false,
25 | currentUser: {},
26 | };
27 | });
28 |
29 | it('calls signup callback', () => {
30 | const navigationComponent = renderComponent();
31 |
32 | navigationComponent.find('NavItem').at(0).simulate('click');
33 |
34 | expect(signup).toHaveBeenCalled();
35 | });
36 |
37 | it('calls signin callback', () => {
38 | const navigationComponent = renderComponent();
39 |
40 | navigationComponent.find('NavItem').at(1).simulate('click');
41 |
42 | expect(signin).toHaveBeenCalled();
43 | });
44 |
45 | context('when user is logged in', () => {
46 | beforeEach(() => {
47 | props = {
48 | ...props,
49 | loggedIn: true,
50 | currentUser: {
51 | id: 1,
52 | name: 'user',
53 | email: 'user@example.com',
54 | },
55 | };
56 | });
57 |
58 | it('calls logout callback', () => {
59 | const navigationComponent = renderComponent();
60 |
61 | navigationComponent.find('NavItem').at(0).simulate('click');
62 |
63 | expect(logout).toHaveBeenCalled();
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies,no-console */
2 | require('dotenv').config();
3 |
4 | const webpack = require('webpack');
5 | const webpackDevMiddleware = require('webpack-dev-middleware');
6 | const webpackHotMiddleware = require('webpack-hot-middleware');
7 | const express = require('express');
8 | const compression = require('compression');
9 | const historyApiFallback = require('connect-history-api-fallback');
10 | const jsonServer = require('./json-server');
11 | const webpackConfig = require('./webpack.config');
12 |
13 | const port = process.env.PORT || 8000;
14 | const server = express();
15 |
16 | if (process.env.NODE_ENV !== 'production') {
17 | const compiler = webpack(webpackConfig);
18 | const webpackOptions = {
19 | stats: {
20 | assets: true,
21 | chunks: false,
22 | modules: false,
23 | colors: true,
24 | performance: true,
25 | timings: true,
26 | version: true,
27 | warnings: true,
28 | },
29 | watchOptions: {
30 | aggregateTimeout: 300,
31 | poll: true,
32 | ignored: /node_modules/,
33 | },
34 | publicPath: webpackConfig.output.publicPath,
35 | };
36 |
37 | compiler.apply(new webpack.ProgressPlugin());
38 | server.use(historyApiFallback());
39 | server.use(webpackDevMiddleware(compiler, webpackOptions));
40 | server.use(webpackHotMiddleware(compiler));
41 | server.listen(port, 'localhost', () => {
42 | console.log(`Server listening on port ${port}`);
43 | });
44 | jsonServer.initialize(server);
45 | } else {
46 | webpack(webpackConfig, (err) => {
47 | if (err) {
48 | console.log(err);
49 | return;
50 | }
51 |
52 | server.use(compression());
53 | server.use(historyApiFallback());
54 | server.use(express.static(webpackConfig.output.path));
55 | server.listen(port);
56 | });
57 | }
58 |
--------------------------------------------------------------------------------
/app/components/todo/tests/__snapshots__/modal.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TodoModal renders correctly 1`] = `
4 |
9 |
41 |
42 | `;
43 |
44 | exports[`TodoModal when form is invalid renders form with validation errors 1`] = `
45 |
50 |
82 |
83 | `;
84 |
--------------------------------------------------------------------------------
/app/components/navigation/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Navbar, Nav, NavItem } from 'react-bootstrap';
4 | import i18n from 'services/i18n';
5 | import { paths } from 'helpers/routes';
6 | import styles from './styles.css';
7 | import NavItemLink from './navItemLink';
8 |
9 | const Navigation = ({
10 | loggedIn, currentUser, logout, signin, signup,
11 | }) => {
12 | const logoutUser = () => {
13 | logout(currentUser);
14 | };
15 |
16 | const signinUser = () => {
17 | signin(currentUser);
18 | };
19 |
20 | const signupUser = () => {
21 | signup(currentUser);
22 | };
23 |
24 | const renderRightNav = () => {
25 | if (loggedIn) {
26 | return (
27 |
28 |
29 | { currentUser.email }
30 |
31 |
32 | { i18n.t('header:signOut') }
33 |
34 |
35 | );
36 | }
37 |
38 | return (
39 |
40 |
41 | { i18n.t('header:signUp') }
42 |
43 |
44 | { i18n.t('header:signIn') }
45 |
46 |
47 | );
48 | };
49 |
50 | return (
51 |
52 |
53 |
54 | { i18n.t('common:projectName') }
55 |
56 |
57 |
58 |
59 | { i18n.t('header:home') }
60 |
61 |
62 | { i18n.t('header:about') }
63 |
64 |
65 | { renderRightNav() }
66 |
67 | );
68 | };
69 |
70 | Navigation.propTypes = {
71 | currentUser: PropTypes.object.isRequired,
72 | loggedIn: PropTypes.bool.isRequired,
73 | logout: PropTypes.func.isRequired,
74 | signin: PropTypes.func.isRequired,
75 | signup: PropTypes.func.isRequired,
76 | };
77 |
78 | export default Navigation;
79 |
--------------------------------------------------------------------------------
/app/reducers/session/index.test.js:
--------------------------------------------------------------------------------
1 | import reducer from './';
2 |
3 | jest.mock('services/sessionStorage', () => ({
4 | loggedIn: () => true,
5 | currentUser: () => ({ id: 1 }),
6 | }));
7 |
8 | describe('Session reducer', () => {
9 | let initialState;
10 | let state;
11 | let actionType;
12 | let payload;
13 |
14 | const callReducer = () => reducer(state, { type: actionType, payload });
15 |
16 | beforeEach(() => {
17 | initialState = {
18 | isLoading: false,
19 | loggedIn: true,
20 | currentUser: { id: 1 },
21 | };
22 | });
23 |
24 | it('returns initial state', () => {
25 | expect(callReducer()).toEqual(initialState);
26 | });
27 |
28 | context('when state is present', () => {
29 | beforeEach(() => {
30 | state = initialState;
31 | });
32 |
33 | describe('LOAD_DATA', () => {
34 | beforeEach(() => {
35 | actionType = 'LOAD_DATA';
36 | });
37 |
38 | it('returns new state', () => {
39 | expect(callReducer()).toEqual({
40 | isLoading: true,
41 | loggedIn: true,
42 | currentUser: { id: 1 },
43 | });
44 | });
45 | });
46 |
47 | describe('SET_USER', () => {
48 | beforeEach(() => {
49 | actionType = 'SET_USER';
50 |
51 | state = {
52 | isLoading: true,
53 | loggedIn: false,
54 | currentUser: {},
55 | };
56 |
57 | payload = {
58 | name: 'User name',
59 | };
60 | });
61 |
62 | it('returns new state', () => {
63 | expect(callReducer()).toEqual({
64 | isLoading: false,
65 | loggedIn: true,
66 | currentUser: { name: 'User name' },
67 | });
68 | });
69 | });
70 |
71 | describe('REMOVE_USER', () => {
72 | beforeEach(() => {
73 | actionType = 'REMOVE_USER';
74 |
75 | state = {
76 | isLoading: true,
77 | loggedIn: true,
78 | currentUser: { name: 'User name' },
79 | };
80 | });
81 |
82 | it('returns new state', () => {
83 | expect(callReducer()).toEqual({
84 | isLoading: false,
85 | loggedIn: false,
86 | currentUser: {},
87 | });
88 | });
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/app/containers/modal/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import { containerWithStore, containerProps } from 'helpers/store';
3 | import Modal from 'containers/modal';
4 |
5 | describe('Modal', () => {
6 | let state;
7 | let component;
8 | const renderComponent = () => shallow(containerWithStore(Modal, state));
9 |
10 | context('when it is todo modal', () => {
11 | beforeEach(() => {
12 | state = {
13 | modal: {
14 | isOpen: true,
15 | modalName: 'todo',
16 | modalOptions: {},
17 | },
18 | };
19 | });
20 |
21 | it('renders TodoModal component', () => {
22 | component = renderComponent().find('ModalContainer').shallow();
23 | const modalProps = containerProps(component);
24 |
25 | expect(component).toBePresent();
26 | expect(component.name()).toEqual('Connect(TodoModal)');
27 | expect(modalProps).toEqual(['isOpen', 'modalOptions', 'closeModal']);
28 | });
29 | });
30 |
31 | context('when it is signin modal', () => {
32 | beforeEach(() => {
33 | state = {
34 | modal: {
35 | isOpen: false,
36 | modalName: 'signin',
37 | modalOptions: {},
38 | },
39 | };
40 | });
41 |
42 | it('renders SigninModal component', () => {
43 | component = renderComponent().find('ModalContainer').shallow();
44 | const modalProps = containerProps(component);
45 |
46 | expect(component).toBePresent();
47 | expect(component.name()).toEqual('Connect(SigninModal)');
48 | expect(modalProps).toEqual(['isOpen', 'modalOptions', 'closeModal']);
49 | });
50 | });
51 |
52 | context('when it is signup modal', () => {
53 | beforeEach(() => {
54 | state = {
55 | modal: {
56 | isOpen: false,
57 | modalName: 'signup',
58 | modalOptions: {},
59 | },
60 | };
61 | });
62 |
63 | it('renders SignupModal component', () => {
64 | component = renderComponent().find('ModalContainer').shallow();
65 | const modalProps = containerProps(component);
66 |
67 | expect(component).toBePresent();
68 | expect(component.name()).toEqual('Connect(SignupModal)');
69 | expect(modalProps).toEqual(['isOpen', 'modalOptions', 'closeModal']);
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/app/components/todo/modal.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-unused-state */
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | FormGroup,
7 | FormControl,
8 | ControlLabel,
9 | } from 'react-bootstrap';
10 | import i18n from 'services/i18n';
11 | import Modal from 'components/modal';
12 | import Form from 'components/form';
13 |
14 | class TodoModal extends Component {
15 | state = {
16 | name: '',
17 | errors: {},
18 | };
19 |
20 | changeName = ({ target }) => {
21 | const { name, value } = target;
22 |
23 | this.setState({ [name]: value });
24 | };
25 |
26 | validationState = () => {
27 | const { length } = this.state.name;
28 |
29 | if (!length) return null;
30 |
31 | return length > 5 ? 'success' : 'error';
32 | };
33 |
34 | createTodo = async (event) => {
35 | event.preventDefault();
36 |
37 | const { name } = this.state;
38 | const { createTodo, closeModal } = this.props;
39 |
40 | if (this.validationState() === 'success') {
41 | try {
42 | await createTodo({ name });
43 | this.setState({ name: '' });
44 | closeModal();
45 | } catch ({ errors }) {
46 | this.setState({ errors });
47 | }
48 | }
49 | };
50 |
51 | render() {
52 | const { name } = this.state;
53 | const { isOpen, closeModal } = this.props;
54 |
55 | return (
56 |
61 |
81 |
82 | );
83 | }
84 | }
85 |
86 | TodoModal.propTypes = {
87 | closeModal: PropTypes.func.isRequired,
88 | createTodo: PropTypes.func.isRequired,
89 | isOpen: PropTypes.bool.isRequired,
90 | };
91 |
92 | export default TodoModal;
93 |
--------------------------------------------------------------------------------
/app/components/signin/tests/__snapshots__/modal.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SigninModal renders correctly 1`] = `
4 |
9 |
55 |
56 | `;
57 |
58 | exports[`SigninModal when form is invalid renders form with validation errors 1`] = `
59 |
64 |
110 |
111 | `;
112 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | const getEntry = () => {
7 | let entry = ['babel-polyfill'];
8 |
9 | if (process.env.NODE_ENV !== 'production') {
10 | entry = entry.concat('react-hot-loader/patch', 'webpack-hot-middleware/client');
11 | }
12 |
13 | return entry.concat('./index');
14 | };
15 |
16 | const getPlugins = () => {
17 | let plugins = [
18 | new HtmlWebpackPlugin({
19 | template: 'index.html',
20 | }),
21 | new ExtractTextPlugin('application.css'),
22 | new webpack.EnvironmentPlugin(['NODE_ENV']),
23 | ];
24 |
25 | if (process.env.NODE_ENV !== 'production') {
26 | plugins = plugins.concat([
27 | new webpack.NamedModulesPlugin(),
28 | new webpack.HotModuleReplacementPlugin(),
29 | new webpack.NoEmitOnErrorsPlugin(),
30 | ]);
31 | } else {
32 | plugins = plugins.concat([
33 | new webpack.optimize.UglifyJsPlugin(),
34 | ]);
35 | }
36 | return plugins;
37 | };
38 |
39 | module.exports = {
40 | context: path.resolve(__dirname, 'app'),
41 | resolve: {
42 | modules: [
43 | path.resolve(__dirname, 'app'),
44 | 'node_modules',
45 | ],
46 | alias: {
47 | config: path.resolve(__dirname, 'app', 'config', 'env', process.env.NODE_ENV || 'development'),
48 | },
49 | extensions: ['.js', '.jsx', '.json'],
50 | },
51 | entry: getEntry(),
52 | output: {
53 | path: path.resolve(__dirname, 'dist'),
54 | publicPath: '/',
55 | filename: 'application.js',
56 | },
57 | devtool: process.env.NODE_ENV !== 'production' ? 'eval-source-map' : undefined,
58 | plugins: getPlugins(),
59 | module: {
60 | rules: [
61 | {
62 | test: /\.js(x)?$/,
63 | exclude: [/node_modules/],
64 | use: ['babel-loader'],
65 | },
66 | {
67 | test: /\.css$/,
68 | exclude: [/app\/stylesheets\//],
69 | use: ExtractTextPlugin.extract({
70 | fallback: 'style-loader',
71 | use: [
72 | {
73 | loader: 'css-loader',
74 | query: {
75 | modules: true,
76 | importLoaders: 1,
77 | },
78 | },
79 | 'postcss-loader',
80 | ],
81 | }),
82 | },
83 | {
84 | test: /\.css$/,
85 | include: [/app\/stylesheets\//],
86 | use: ExtractTextPlugin.extract({
87 | fallback: 'style-loader',
88 | use: [
89 | 'css-loader',
90 | 'postcss-loader',
91 | ],
92 | }),
93 | },
94 | {
95 | test: /\.(jpg|png|ttf|eot|svg|woff2|woff)$/,
96 | use: ['file-loader'],
97 | },
98 | ],
99 | },
100 | };
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skeleton for React based application
2 |
3 | [](https://travis-ci.org/fs/react-base)
4 | [](https://david-dm.org/fs/react-base)
5 | [](https://david-dm.org/fs/react-base#info=devDependencies)
6 |
7 | Kick-start your new web application based on React and Redux technologies. It also includes Webpack 3, Yarn, React hot loader, PostCSS, JSON-server tools and test environment based on Jest, Enzyme, Eslint and Stylelint for even more rapid development.
8 |
9 | Try the demo [http://react-base.herokuapp.com](http://react-base.herokuapp.com)
10 |
11 | ## Dependencies:
12 |
13 | List of all dependencies is presented [here](https://github.com/fs/react-base/blob/master/package.json)
14 |
15 | ## Install
16 | ### OSX
17 |
18 | Install Node.js
19 |
20 | ```bash
21 | brew install nvm yarn
22 | nvm install node
23 | nvm alias default node
24 | ```
25 |
26 | ## Quick start
27 |
28 | Clone application as new project with original repository named "react-base"
29 |
30 | ```bash
31 | git clone git@github.com:fs/react-base.git --origin react-base [MY-NEW-PROJECT]
32 | ```
33 |
34 | Create your new repo on GitHub and push master into it.
35 | Make sure master branch is tracking origin repo.
36 |
37 | ```bash
38 | git remote add origin git@github.com:[MY-GITHUB-ACCOUNT]/[MY-NEW-PROJECT].git
39 | git push -u origin master
40 | ```
41 |
42 | Run bootstrap script
43 |
44 | ```bash
45 | bin/setup
46 | ```
47 |
48 | ## Run application
49 |
50 | Run app (by default environment is 'development', port is 8000)
51 |
52 | ```bash
53 | yarn start
54 | ```
55 |
56 | Run app with options
57 |
58 | ```bash
59 | [] yarn start
60 | ```
61 |
62 | ```bash
63 | NODE_ENV=development # build app with development environment
64 | NODE_ENV=production # build app with production environment
65 | NODE_ENV=test # build app with test environment
66 | PORT=8000 # run server on 8000 port
67 | ```
68 |
69 | Start to use application in browser:
70 |
71 | ```bash
72 | localhost:8000
73 | ```
74 |
75 | ## Run tests and linters
76 |
77 | ```bash
78 | yarn test
79 | ```
80 |
81 | ## Code linting tasks
82 |
83 | Run javascript linter
84 | ```bash
85 | yarn eslint
86 | ```
87 |
88 | Run stylesheets linter
89 | ```bash
90 | yarn stylelint
91 | ```
92 |
93 | Run all linters
94 | ```bash
95 | yarn lint
96 | ```
97 |
98 | ## Test tasks
99 |
100 | Run jest tests
101 | ```bash
102 | yarn jest
103 | ```
104 |
105 | ## Credits
106 |
107 | React base is maintained by [Marat Fakhreev](http://github.com/maratfakhreev).
108 | It was written by [Flatstack](http://www.flatstack.com) with the help of our
109 | [contributors](http://github.com/fs/react-base/contributors).
110 |
111 | [ ](http://www.flatstack.com)
112 |
--------------------------------------------------------------------------------
/app/components/signin/modal.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-unused-state */
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | FormGroup,
7 | FormControl,
8 | ControlLabel,
9 | } from 'react-bootstrap';
10 | import i18n from 'services/i18n';
11 | import Modal from 'components/modal';
12 | import Form from 'components/form';
13 |
14 | class SigninModal extends Component {
15 | state = {
16 | email: '',
17 | password: '',
18 | errors: {},
19 | };
20 |
21 | setValue = ({ target }) => {
22 | const { name, value } = target;
23 |
24 | this.setState({ [name]: value });
25 | };
26 |
27 | isFormValid = () => {
28 | const { email, password } = this.state;
29 |
30 | return email.length > 5 && password.length > 5;
31 | };
32 |
33 | signIn = async (event) => {
34 | event.preventDefault();
35 |
36 | const { email, password } = this.state;
37 | const { signinUser, closeModal } = this.props;
38 |
39 | if (this.isFormValid()) {
40 | try {
41 | await signinUser({ email, password });
42 | this.setState({ email: '', password: '' });
43 | closeModal();
44 | } catch ({ errors }) {
45 | this.setState({ errors });
46 | }
47 | }
48 | };
49 |
50 | validationState = (value) => {
51 | const { length } = value;
52 |
53 | if (!length) return null;
54 |
55 | return length > 5 ? 'success' : 'error';
56 | };
57 |
58 | render() {
59 | const { email, password } = this.state;
60 | const {
61 | isOpen,
62 | closeModal,
63 | session: {
64 | isLoading,
65 | },
66 | } = this.props;
67 |
68 | return (
69 |
74 |
110 |
111 | );
112 | }
113 | }
114 |
115 | SigninModal.propTypes = {
116 | closeModal: PropTypes.func.isRequired,
117 | isOpen: PropTypes.bool.isRequired,
118 | session: PropTypes.object.isRequired,
119 | signinUser: PropTypes.func.isRequired,
120 | };
121 |
122 | export default SigninModal;
123 |
--------------------------------------------------------------------------------
/app/reducers/todos/index.test.js:
--------------------------------------------------------------------------------
1 | import reducer from './';
2 |
3 | describe('Todos reducer', () => {
4 | let initialState;
5 | let state;
6 | let actionType;
7 | let payload;
8 |
9 | const callReducer = () => reducer(state, { type: actionType, payload });
10 |
11 | beforeEach(() => {
12 | initialState = {
13 | isLoading: false,
14 | todos: [],
15 | };
16 | });
17 |
18 | it('returns initial state', () => {
19 | expect(callReducer()).toEqual(initialState);
20 | });
21 |
22 | context('when state is present', () => {
23 | beforeEach(() => {
24 | state = initialState;
25 | });
26 |
27 | describe('LOAD_TODOS', () => {
28 | beforeEach(() => {
29 | actionType = 'LOAD_TODOS';
30 | });
31 |
32 | it('returns new state', () => {
33 | expect(callReducer()).toEqual({
34 | isLoading: true,
35 | todos: [],
36 | });
37 | });
38 | });
39 |
40 | describe('SET_TODOS', () => {
41 | beforeEach(() => {
42 | actionType = 'SET_TODOS';
43 |
44 | state.isLoading = true;
45 |
46 | payload = [{
47 | id: 1,
48 | name: 'Awesome Todo',
49 | }];
50 | });
51 |
52 | it('returns new state', () => {
53 | expect(callReducer()).toEqual({
54 | isLoading: false,
55 | todos: [{
56 | id: 1,
57 | name: 'Awesome Todo',
58 | }],
59 | });
60 | });
61 | });
62 |
63 | describe('ADD_TODO', () => {
64 | beforeEach(() => {
65 | actionType = 'ADD_TODO';
66 |
67 | state.todos = [{
68 | id: 2,
69 | name: 'Another Todo',
70 | }];
71 |
72 | payload = {
73 | id: 1,
74 | name: 'Awesome Todo',
75 | };
76 | });
77 |
78 | it('returns new state', () => {
79 | expect(callReducer()).toEqual({
80 | isLoading: false,
81 | todos: [{
82 | id: 2,
83 | name: 'Another Todo',
84 | }, {
85 | id: 1,
86 | name: 'Awesome Todo',
87 | }],
88 | });
89 | });
90 | });
91 |
92 | describe('TOGGLE_TODO', () => {
93 | beforeEach(() => {
94 | actionType = 'TOGGLE_TODO';
95 |
96 | state.todos = [{
97 | id: 1,
98 | name: 'Awesome Todo',
99 | }, {
100 | id: 2,
101 | name: 'Another Todo',
102 | }];
103 |
104 | payload = {
105 | id: 1,
106 | name: 'Updated Todo',
107 | };
108 | });
109 |
110 | it('returns new state', () => {
111 | expect(callReducer()).toEqual({
112 | isLoading: false,
113 | todos: [{
114 | id: 1,
115 | name: 'Updated Todo',
116 | }, {
117 | id: 2,
118 | name: 'Another Todo',
119 | }],
120 | });
121 | });
122 | });
123 |
124 | describe('REMOVE_TODO', () => {
125 | beforeEach(() => {
126 | actionType = 'REMOVE_TODO';
127 |
128 | state.todos = [{
129 | id: 1,
130 | name: 'Awesome Todo',
131 | }, {
132 | id: 2,
133 | name: 'Another Todo',
134 | }];
135 |
136 | payload = {
137 | id: 1,
138 | name: 'Awesome Todo',
139 | };
140 | });
141 |
142 | it('returns new state', () => {
143 | expect(callReducer()).toEqual({
144 | isLoading: false,
145 | todos: [{
146 | id: 2,
147 | name: 'Another Todo',
148 | }],
149 | });
150 | });
151 | });
152 | });
153 | });
154 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-base",
3 | "version": "2.0.0",
4 | "authors": [
5 | "Marat Fakhreev "
6 | ],
7 | "repository": {
8 | "type": "git",
9 | "url": "git://github.com:fs/react-base.git"
10 | },
11 | "scripts": {
12 | "stylelint": "stylelint './app/**/*.css'",
13 | "stylelint-fix": "stylefmt --recursive './app/**/*.css'",
14 | "eslint": "eslint 'server.js' './app' './config' --ext .js,.jsx",
15 | "eslint-fix": "yarn eslint -- --fix",
16 | "lint": "yarn stylelint && yarn eslint",
17 | "jest": "jest",
18 | "test": "yarn lint && yarn jest",
19 | "start": "node ./server"
20 | },
21 | "license": "MIT",
22 | "engines": {
23 | "node": ">=9.0.0"
24 | },
25 | "dependencies": {
26 | "axios": "^0.18.0",
27 | "bootstrap": "^3.3.7",
28 | "classnames": "^2.2.5",
29 | "history": "^4.7.2",
30 | "i18next": "^10.4.1",
31 | "i18next-browser-languagedetector": "^2.0.0",
32 | "lodash": "^4.17.4",
33 | "path-to-regexp": "^2.1.0",
34 | "prop-types": "^15.6.0",
35 | "react": "^16.2.0",
36 | "react-bootstrap": "^0.32.1",
37 | "react-dom": "^16.2.0",
38 | "react-hot-loader": "^4.0.0-beta.18",
39 | "react-redux": "^5.0.6",
40 | "react-router": "^4.2.0",
41 | "react-router-dom": "^4.2.2",
42 | "react-s-alert": "^1.4.1",
43 | "redux": "^3.7.2",
44 | "redux-actions": "^2.2.1",
45 | "redux-devtools-extension": "^2.13.2",
46 | "redux-thunk": "^2.2.0"
47 | },
48 | "devDependencies": {
49 | "autoprefixer": "^8.0.0",
50 | "babel-core": "^6.26.0",
51 | "babel-eslint": "^8.2.2",
52 | "babel-jest": "^22.2.2",
53 | "babel-loader": "^7.1.2",
54 | "babel-plugin-module-resolver": "^3.1.0",
55 | "babel-plugin-transform-react-constant-elements": "^6.23.0",
56 | "babel-plugin-transform-react-inline-elements": "^6.22.0",
57 | "babel-plugin-transform-react-remove-prop-types": "^0.4.8",
58 | "babel-polyfill": "^6.26.0",
59 | "babel-preset-env": "^1.6.1",
60 | "babel-preset-react": "^6.24.1",
61 | "babel-preset-stage-0": "^6.24.1",
62 | "compression": "^1.7.0",
63 | "connect-history-api-fallback": "^1.3.0",
64 | "css-loader": "^0.28.5",
65 | "dotenv": "^5.0.0",
66 | "enzyme": "^3.3.0",
67 | "enzyme-adapter-react-16": "^1.1.1",
68 | "enzyme-matchers": "^4.2.0",
69 | "enzyme-to-json": "^3.3.1",
70 | "eslint": "^4.5.0",
71 | "eslint-config-airbnb": "^16.1.0",
72 | "eslint-import-resolver-jest": "^2.0.1",
73 | "eslint-import-resolver-webpack": "^0.8.4",
74 | "eslint-plugin-import": "^2.8.0",
75 | "eslint-plugin-jest": "^21.12.2",
76 | "eslint-plugin-jsx-a11y": "^6.0.3",
77 | "eslint-plugin-react": "^7.7.0",
78 | "express": "^4.15.4",
79 | "extract-text-webpack-plugin": "^3.0.0",
80 | "file-loader": "^1.1.7",
81 | "html-webpack-plugin": "^2.30.1",
82 | "identity-obj-proxy": "^3.0.0",
83 | "jest": "^22.3.0",
84 | "jest-enzyme": "^4.2.0",
85 | "json-server": "^0.12.0",
86 | "postcss-color-function": "^4.0.0",
87 | "postcss-extend": "^1.0.5",
88 | "postcss-import": "^11.1.0",
89 | "postcss-loader": "^2.0.6",
90 | "postcss-mixins": "^6.1.0",
91 | "postcss-nested": "^3.0.0",
92 | "postcss-pxtorem": "^4.0.1",
93 | "postcss-simple-vars": "^4.1.0",
94 | "react-test-renderer": "^16.2.0",
95 | "redux-mock-store": "^1.2.3",
96 | "style-loader": "^0.20.2",
97 | "stylefmt": "^6.0.0",
98 | "stylelint": "^9.0.0",
99 | "stylelint-config-fs": "^0.5.0",
100 | "url-loader": "^0.6.2",
101 | "webpack": "^3.5.5",
102 | "webpack-dev-middleware": "^2.0.5",
103 | "webpack-hot-middleware": "^2.18.2"
104 | },
105 | "jest": {
106 | "modulePaths": [
107 | "app",
108 | "test"
109 | ],
110 | "testPathIgnorePatterns": [
111 | "/node_modules/",
112 | "config"
113 | ],
114 | "moduleFileExtensions": [
115 | "js",
116 | "jsx"
117 | ],
118 | "moduleNameMapper": {
119 | "\\.css$": "identity-obj-proxy"
120 | },
121 | "setupFiles": [
122 | "./test/mocks/fakeStorage.js"
123 | ],
124 | "setupTestFrameworkScriptFile": "./test/setup.js"
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/components/signup/tests/__snapshots__/modal.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SignupModal renders correctly 1`] = `
4 |
9 |
88 |
89 | `;
90 |
91 | exports[`SignupModal when form is invalid renders form with validation errors 1`] = `
92 |
97 |
176 |
177 | `;
178 |
--------------------------------------------------------------------------------
/app/components/navigation/tests/__snapshots__/index.snapshot.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Navigation renders correctly 1`] = `
4 |
7 |
10 |
13 |
16 | React-base
17 |
18 |
19 |
56 |
89 |
90 |
91 | `;
92 |
93 | exports[`Navigation when user is logged in renders user navigations 1`] = `
94 |
97 |
100 |
103 |
106 | React-base
107 |
108 |
109 |
146 |
181 |
182 |
183 | `;
184 |
--------------------------------------------------------------------------------
/app/components/signup/modal.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-unused-state */
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import {
5 | Button,
6 | FormGroup,
7 | FormControl,
8 | ControlLabel,
9 | } from 'react-bootstrap';
10 | import i18n from 'services/i18n';
11 | import Modal from 'components/modal';
12 | import Form from 'components/form';
13 |
14 | class SignupModal extends Component {
15 | state = {
16 | name: '',
17 | email: '',
18 | password: '',
19 | passwordConfirmation: '',
20 | errors: {},
21 | };
22 |
23 | setValue = ({ target }) => {
24 | const { name, value } = target;
25 |
26 | this.setState({ [name]: value });
27 | };
28 |
29 | isFormValid = () => {
30 | const {
31 | name,
32 | email,
33 | password,
34 | passwordConfirmation,
35 | } = this.state;
36 |
37 | return (
38 | name.trim().length &&
39 | email.length > 5 &&
40 | password.length > 5 &&
41 | passwordConfirmation.length > 5 &&
42 | this.isValidPassword()
43 | );
44 | };
45 |
46 | isValidPassword = () => {
47 | const { password, passwordConfirmation } = this.state;
48 |
49 | return password === passwordConfirmation;
50 | };
51 |
52 | validationState = (value) => {
53 | const { length } = value;
54 |
55 | if (!length) return null;
56 |
57 | return length > 5 ? 'success' : 'error';
58 | };
59 |
60 | nameValidationState = (value) => {
61 | const { length } = value.trim();
62 |
63 | return length ? 'success' : null;
64 | };
65 |
66 | passwordValidationState = (value) => {
67 | const { length } = value;
68 |
69 | if (!length) return null;
70 |
71 | return (this.isValidPassword() && length > 5) ? 'success' : 'error';
72 | };
73 |
74 | signUp = async (event) => {
75 | event.preventDefault();
76 |
77 | const {
78 | name,
79 | email,
80 | password,
81 | passwordConfirmation,
82 | } = this.state;
83 | const { signupUser, closeModal } = this.props;
84 |
85 | if (this.isFormValid()) {
86 | try {
87 | await signupUser({
88 | name, email, password, passwordConfirmation,
89 | });
90 | this.setState({
91 | name: '',
92 | email: '',
93 | password: '',
94 | passwordConfirmation: '',
95 | });
96 | closeModal();
97 | } catch ({ errors }) {
98 | this.setState({ errors });
99 | }
100 | }
101 | };
102 |
103 | render() {
104 | const {
105 | name,
106 | email,
107 | password,
108 | passwordConfirmation,
109 | } = this.state;
110 | const {
111 | isOpen,
112 | closeModal,
113 | session: {
114 | isLoading,
115 | },
116 | } = this.props;
117 |
118 | return (
119 |
124 |
177 |
178 | );
179 | }
180 | }
181 |
182 | SignupModal.propTypes = {
183 | closeModal: PropTypes.func.isRequired,
184 | isOpen: PropTypes.bool.isRequired,
185 | session: PropTypes.object.isRequired,
186 | signupUser: PropTypes.func.isRequired,
187 | };
188 |
189 | export default SignupModal;
190 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unreleased
4 | - Introduce async/await for async requests. Use axios instead of j-fetch. Move json-server logic to separated module
5 | ([#100](https://github.com/fs/react-base/pull/100))
6 | - Add "env" preset and move some plugins to production environment
7 | ([#98](https://github.com/fs/react-base/pull/98))
8 | - Setup HMR for reducers
9 | ([#96](https://github.com/fs/react-base/pull/96))
10 | - Setup env variable usage on client side
11 | ([#95](https://github.com/fs/react-base/pull/95))
12 | - Require reducers automatically via webpack require.context
13 | ([#94](https://github.com/fs/react-base/pull/94))
14 |
15 | ## 2.0.0 - 2017-11-12
16 | - Upgrade Node.js to version 9. Update hmr setup code regarding to webpack 3
17 | ([#92](https://github.com/fs/react-base/pull/92))
18 | - Remove mirror-creator. Move constants declaration to actions. Refactor actions and reducers.
19 | ([#91](https://github.com/fs/react-base/pull/91))
20 | - Add test environment, snapshot and unit tests. Refactor containers.
21 | ([#79](https://github.com/fs/react-base/pull/79))
22 | - Upgrade dependencies. Remove react-transition-group. Remove webpack-dashboard
23 | ([#88](https://github.com/fs/react-base/pull/88))
24 | - Add localization using i18next and react-i18next
25 | ([#83](https://github.com/fs/react-base/pull/83))
26 | - Use j-fetch library as main request library
27 | ([#85](https://github.com/fs/react-base/pull/85))
28 | - Use major version of Node.js in .nvmrc and package.json
29 | ([#82](https://github.com/fs/react-base/pull/82))
30 | - Cleanup postcss config
31 | ([#81](https://github.com/fs/react-base/pull/81))
32 | - Refactor stylesheets. Add react-transition-group, postcss-extend
33 | ([#80](https://github.com/fs/react-base/pull/80))
34 | - Remove deprecated postcss-inline-comment and react-addons-css-transition-group.
35 | ([#80](https://github.com/fs/react-base/pull/80))
36 | - Upgrade dependencies
37 | ([#80](https://github.com/fs/react-base/pull/80))
38 | - Upgrade dependencies
39 | ([#77](https://github.com/fs/react-base/pull/77))
40 | - Make improvements in the http lib
41 | ([#77](https://github.com/fs/react-base/pull/77))
42 | - Use stylelint-config-fs for linting CSS
43 | ([#76](https://github.com/fs/react-base/pull/76))
44 | - Migrate to React 15.5.x
45 | ([#76](https://github.com/fs/react-base/pull/76))
46 | - Upgrade dependencies
47 | ([#76](https://github.com/fs/react-base/pull/76))
48 | - Upgrade Node.js to 7.8.x
49 | ([#68](https://github.com/fs/react-base/pull/68))
50 | - Upgrade dependencies
51 | ([#68](https://github.com/fs/react-base/pull/68))
52 | - Cleanup eslint config
53 | ([#68](https://github.com/fs/react-base/pull/68))
54 | - Introduce Redux as a main unidirectional architecture realization
55 | ([#60](https://github.com/fs/react-base/pull/60))
56 | - Introduce custom http lib based on fetch-api as a main tool for server requests
57 | ([#60](https://github.com/fs/react-base/pull/60))
58 | - Introduce yarn
59 | ([#67](https://github.com/fs/react-base/pull/67))
60 |
61 | ## 1.1.0 - 2017-01-24
62 | - Upgrade dependencies
63 | ([#65](https://github.com/fs/react-base/pull/65))
64 | - Upgrade Node.js to 7.4.x
65 | ([#63](https://github.com/fs/react-base/pull/63))
66 | - Add transform-react-constant-elements, transform-react-inline-elements babel plugins
67 | ([#63](https://github.com/fs/react-base/pull/63))
68 | - Setup jest as default testing framework instead of karma
69 | ([#63](https://github.com/fs/react-base/pull/63))
70 | - Upgrade dependencies
71 | ([#63](https://github.com/fs/react-base/pull/63))
72 | - Add webpack dashboard
73 | ([#63](https://github.com/fs/react-base/pull/63))
74 | - Migrate to Webpack 2. Setup React-hot-loader 3
75 | ([#63](https://github.com/fs/react-base/pull/63))
76 | - Upgrade Node.js to 7.2.x
77 | ([#62](https://github.com/fs/react-base/pull/62))
78 | - Upgrade dependencies
79 | ([#62](https://github.com/fs/react-base/pull/62))
80 | - Use eslint-config-fs for linting javascript
81 | ([#62](https://github.com/fs/react-base/pull/62))
82 | - Upgrade Node.js to 6.7.x
83 | ([#57](https://github.com/fs/react-base/pull/57))
84 | - Upgrade eslint rules
85 | ([#57](https://github.com/fs/react-base/pull/57))
86 | - Upgrade dependencies
87 | ([#57](https://github.com/fs/react-base/pull/57))
88 | - Add json loader. Cleanup webpack config files
89 | ([#57](https://github.com/fs/react-base/pull/57))
90 | - Use react entities as self imported
91 | ([#56](https://github.com/fs/react-base/pull/56))
92 |
93 | ## 1.0.0 - 2016-09-15
94 | - Upgrade dependencies
95 | ([#55](https://github.com/fs/react-base/pull/55))
96 | - Upgrade dependencies
97 | ([#54](https://github.com/fs/react-base/pull/54))
98 | - Add Todo Modal test
99 | ([#53](https://github.com/fs/react-base/pull/53))
100 | - Add Sign Up Modal test
101 | ([#52](https://github.com/fs/react-base/pull/52))
102 | - Add Sign In Modal test
103 | ([#51](https://github.com/fs/react-base/pull/51))
104 | - Bring eslint rules to general view
105 | ([#50](https://github.com/fs/react-base/pull/50))
106 | - Add assets compression.
107 | ([#49](https://github.com/fs/react-base/pull/49))
108 | - Upgrade dependencies. Add new stylelint/eslint rules.
109 | ([#48](https://github.com/fs/react-base/pull/48))
110 | - Upgrade Node.js to 6.5.x
111 | ([#47](https://github.com/fs/react-base/pull/47))
112 | - Prefer camelCase to js files
113 | ([#47](https://github.com/fs/react-base/pull/47))
114 | - Add new rules to stylelint config
115 | ([#47](https://github.com/fs/react-base/pull/47))
116 | - Upgrade dependencies
117 | ([#46](https://github.com/fs/react-base/pull/46))
118 | - Remove functions binding inside jsx
119 | ([#46](https://github.com/fs/react-base/pull/46))
120 | - Upgrade Node.js to 6.4.x
121 | ([#44](https://github.com/fs/react-base/pull/44))
122 | - Add .gitattributes to avoid merge conflicts
123 | ([#44](https://github.com/fs/react-base/pull/44))
124 | - Upgrade dependencies. Simplify environment configs
125 | ([#43](https://github.com/fs/react-base/pull/43))
126 | - Introduce appHistory service. Update custom request lib. Update routes helper
127 | ([#42](https://github.com/fs/react-base/pull/42))
128 | - Extract session actions into session service
129 | ([#41](https://github.com/fs/react-base/pull/41))
130 | - Add new rules in postcss sorting config
131 | ([#39](https://github.com/fs/react-base/pull/39))
132 | - Upgrade Node.js to 6.3.x
133 | ([#38](https://github.com/fs/react-base/pull/38))
134 | - Upgrade dependencies
135 | ([#37](https://github.com/fs/react-base/pull/37))
136 | - Remove warnings in production build and update react-bootstrap
137 | ([#36](https://github.com/fs/react-base/pull/36))
138 | - Add postcss sorting config and rule for stylelint properties order
139 | ([#35](https://github.com/fs/react-base/pull/35))
140 | - Rewrite modals logic and add main Modals component. Clean up code
141 | ([#33](https://github.com/fs/react-base/pull/33))
142 | - Upgrade dependencies
143 | ([#31](https://github.com/fs/react-base/pull/31))
144 | - Add qs lib. Add ability to pass query parameters into request url
145 | ([#32](https://github.com/fs/react-base/pull/32))
146 | - Add classnames utility for conditionally joining classNames together
147 | ([#32](https://github.com/fs/react-base/pull/32))
148 | - Reorganize components structure
149 | ([#29](https://github.com/fs/react-base/pull/29))
150 | - Change javascript files extensions from .jsx to .js
151 | ([#29](https://github.com/fs/react-base/pull/29))
152 | - Fix stylesheets hot reloading issue
153 | ([#29](https://github.com/fs/react-base/pull/29))
154 | - Fix issue related to server port for production server
155 | ([#28](https://github.com/fs/react-base/pull/28))
156 | - Rewrite all scripts without usage of gulp. Remove gulp from project
157 | ([#27](https://github.com/fs/react-base/pull/27))
158 | - Upgrade Node.js to 6.2.x
159 | ([#26](https://github.com/fs/react-base/pull/26))
160 | - Lock core-decorators package version to avoid tests errors
161 | - Upgrade dependencies
162 | ([#25](https://github.com/fs/react-base/pull/25))
163 | - Upgrade dependencies
164 | ([#20](https://github.com/fs/react-base/pull/20))
165 | - Upgrade es6-promise polyfill lib
166 | ([#19](https://github.com/fs/react-base/pull/19))
167 | - Setup karma and jasmine for test environment
168 | ([#11](https://github.com/fs/react-base/pull/11))
169 | - Upgrade Node.js version to 6.1.0
170 | ([#18](https://github.com/fs/react-base/pull/18))
171 | - Remove stylelint-statement-max-nesting-depth since stylelint contains this option
172 | ([#17](https://github.com/fs/react-base/pull/17))
173 | - Upgrade dependencies
174 | ([#17](https://github.com/fs/react-base/pull/17))
175 | - Replace abstract actions/stores with their decorator analogs
176 | ([#12](https://github.com/fs/react-base/pull/12))
177 | - Add lodash to the devDependencies to avoid problem with json-server's resolved lodash version
178 | ([#16](https://github.com/fs/react-base/pull/16))
179 | - Add sign up functionality
180 | ([#8](https://github.com/fs/react-base/pull/8))
181 | - Introduce abstract actions and stores
182 | ([#8](https://github.com/fs/react-base/pull/8))
183 | - Introduce props validation for components
184 | ([#10](https://github.com/fs/react-base/pull/10))
185 | - Upgrade Node.js version to 6.0.0
186 | ([#9](https://github.com/fs/react-base/pull/9))
187 | - Replace deprecated Input on FormGroup in react-bootstrap
188 | ([#7](https://github.com/fs/react-base/pull/7))
189 | - Implement sign in form validation
190 | ([#7](https://github.com/fs/react-base/pull/7))
191 | - Fix setup script to avoid issue with application bootstraping
192 | ([#6](https://github.com/fs/react-base/pull/6))
193 | - Upgrade dependencies
194 | ([#5](https://github.com/fs/react-base/pull/5))
195 | - Upgrade React to 15.0.0
196 | ([#2](https://github.com/fs/react-base/pull/2))
197 |
--------------------------------------------------------------------------------