├── .babelrc ├── .flowconfig ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── assets ├── 512.png ├── favicon.ico └── manifest.json ├── package.json ├── src ├── actions │ └── index.js ├── components │ ├── App.js │ ├── Auth │ │ ├── Form.js │ │ ├── Login.js │ │ └── Signup.js │ ├── Header │ │ ├── Github.js │ │ ├── __test__ │ │ │ ├── Github.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Github.test.js.snap │ │ └── index.js │ ├── Home.js │ └── Styled.js ├── constants │ ├── actionTypes.js │ ├── index.js │ └── urls.js ├── helpers │ ├── fetch.js │ ├── index.js │ └── persist.js ├── index.js ├── reducers │ ├── index.js │ └── user.js └── store.js ├── webpack ├── hotReload.js ├── template.html ├── webpack.config.dev.js └── webpack.config.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "react", 10 | "stage-0" 11 | ], 12 | "plugins": [ 13 | [ 14 | "emotion/babel", { 15 | "inline": true 16 | } 17 | ], 18 | [ 19 | "transform-runtime", { 20 | "helpers": false, 21 | "polyfill": false 22 | }, 23 | ] 24 | ], 25 | "env": { 26 | "test": { 27 | "plugins": [ 28 | "transform-es2015-modules-commonjs" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | cache: 5 | yarn: true 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Didier Franc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 2 | 3 | ### [Live](https://react.didierfranc.com/) 4 | 5 | # redux-react-starter 6 | 7 | This repository contains the minimal app to get started with `redux`, `react`, `hot-reloading`, `async function` and some other great stuffs. 8 | 9 | ## How to 10 | 11 | [yarn](https://github.com/yarnpkg/yarn) 0.18+ must be present on your machine. 12 | 13 | ### Start 14 | 15 | Run webpack-dev-server, get ready to code with hot reloading 16 | ``` 17 | yarn start 18 | ``` 19 | 20 | ## Share 21 | 22 | Share your localhost running app to anyone with an internet connection 23 | ``` 24 | yarn ngrok 25 | ``` 26 | 27 | ### Build 28 | 29 | Bundle your app. It will create `index.html`, `main.[hash].js`, `vendor.[hash].js` and `manifest.[hash].js` 30 | ``` 31 | yarn build 32 | ``` 33 | 34 | ### Run your build 35 | ``` 36 | yarn prod 37 | ``` 38 | 39 | ### Deploy 40 | 41 | #### [Surge.sh](http://surge.sh) 42 | ``` 43 | surge ./dist -d subdomain.surge.sh 44 | ``` 45 | 46 | #### [Github Pages](https://help.github.com/articles/configuring-a-publishing-source-for-github-pages/) 47 | ``` 48 | mv dist docs 49 | git push upstream master 50 | ``` 51 | 52 | Then go to your repository, Settings -> Options -> Github Pages and select /docs folder 53 | 54 | ## What's inside ? 55 | 56 | 👉 [package.json](https://github.com/didierfranc/redux-react-starter/blob/master/package.json) 57 | 58 | ## Tools 59 | 60 | If you have not already done so, move to **Chrome** and install [react-developer-tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) & [redux-devtools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) 61 | 62 | ## Create-React-App 63 | 64 | If you don't care about the process or you don't want to play with your config try [create-react-app](https://github.com/facebookincubator/create-react-app) 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /assets/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didierfranc/redux-react-starter/f5306352c5f62a35f705e00cfd990cf01182aab8/assets/512.png -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didierfranc/redux-react-starter/f5306352c5f62a35f705e00cfd990cf01182aab8/assets/favicon.ico -------------------------------------------------------------------------------- /assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-react-starter", 3 | "short_name": "React", 4 | "lang": "fr", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "theme_color": "#0080FF", 8 | "background_color": "#FFFFFF", 9 | "icons": [ 10 | { 11 | "src": "512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-react-starter", 3 | "version": "1.2.0", 4 | "description": "Get started with ES2015, React and Redux. Including Webpack, ESLint, react-router, fetch ...", 5 | "scripts": { 6 | "start": "webpack-dev-server --open --config webpack/webpack.config.dev.js", 7 | "build": "rm -rf dist && webpack --config webpack/webpack.config.prod.js && cp -a assets/ dist/", 8 | "prod": "serve ./dist -s", 9 | "lint": "eslint src", 10 | "ngrok": "ngrok http -region eu 8080", 11 | "test": "jest", 12 | "precommit": "lint-staged" 13 | }, 14 | "lint-staged": { 15 | "*.js": [ 16 | "prettier --write --no-semi --single-quote --trailing-comma all", 17 | "git add" 18 | ] 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/didierfranc/redux-react-starter.git" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "redux", 27 | "async" 28 | ], 29 | "author": "Didier Franc", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/didierfranc/redux-react-starter/issues" 33 | }, 34 | "homepage": "https://github.com/didierfranc/redux-react-starter#readme", 35 | "devDependencies": { 36 | "babel-core": "^6.25.0", 37 | "babel-eslint": "^7.2.3", 38 | "babel-jest": "^20.0.3", 39 | "babel-loader": "^7.1.1", 40 | "babel-plugin-transform-runtime": "^6.23.0", 41 | "babel-preset-es2015": "^6.24.1", 42 | "babel-preset-react": "^6.24.1", 43 | "babel-preset-stage-0": "^6.24.1", 44 | "babel-runtime": "^6.23.0", 45 | "eslint": "^3.19.0", 46 | "eslint-config-airbnb": "^15.0.2", 47 | "eslint-plugin-import": "^2.7.0", 48 | "eslint-plugin-jsx-a11y": "^5.1.1", 49 | "eslint-plugin-react": "^7.1.0", 50 | "html-webpack-plugin": "^2.29.0", 51 | "husky": "^0.14.3", 52 | "jest": "^21.1.0", 53 | "lint-staged": "^4.0.1", 54 | "preload-webpack-plugin": "^1.2.2", 55 | "prettier": "^1.5.2", 56 | "react-test-renderer": "^16.0.0", 57 | "serve": "^6.0.2", 58 | "webpack": "^3.1.0", 59 | "webpack-dev-server": "^2.5.1" 60 | }, 61 | "dependencies": { 62 | "emotion": "^7.3.2", 63 | "lodash": "^4.17.4", 64 | "offline-plugin": "^4.8.3", 65 | "react": "^16.0.0", 66 | "react-code-splitting": "^1.1.1", 67 | "react-dom": "^16.0.0", 68 | "react-redux": "^5.0.5", 69 | "react-router-dom": "^4.1.1", 70 | "redux": "^3.7.1", 71 | "redux-thunk": "^2.2.0" 72 | }, 73 | "eslintConfig": { 74 | "env": { 75 | "browser": true, 76 | "jest": true 77 | }, 78 | "extends": "airbnb", 79 | "parser": "babel-eslint", 80 | "settings": { 81 | "import/resolver": { 82 | "webpack": { 83 | "config": "webpack/webpack.config.dev.js" 84 | } 85 | } 86 | }, 87 | "rules": { 88 | "arrow-parens": [ 89 | "error", 90 | "as-needed" 91 | ], 92 | "no-confusing-arrow": 0, 93 | "no-shadow": 0, 94 | "no-underscore-dangle": 0, 95 | "semi": [ 96 | 1, 97 | "never" 98 | ], 99 | "import/no-extraneous-dependencies": 0, 100 | "import/prefer-default-export": 0, 101 | "import/no-duplicates": 0, 102 | "react/jsx-filename-extension": [ 103 | 1, 104 | { 105 | "extensions": [ 106 | ".js" 107 | ] 108 | } 109 | ] 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { actionTypes as types, urls } from '../constants' 2 | import { post } from '../helpers' 3 | 4 | export const signup = ({ email, password }) => dispatch => { 5 | dispatch({ type: types.SIGNUP_REQUEST }) 6 | post({ 7 | url: urls.SIGNUP, 8 | body: { email, password }, 9 | success: types.SIGNUP_SUCCESS, 10 | failure: types.SIGNUP_FAILURE, 11 | dispatch, 12 | }) 13 | } 14 | 15 | export const login = ({ email, password }) => dispatch => { 16 | dispatch({ type: types.LOGIN_REQUEST }) 17 | post({ 18 | url: urls.LOGIN, 19 | body: { email, password }, 20 | success: types.LOGIN_SUCCESS, 21 | failure: types.LOGIN_FAILURE, 22 | dispatch, 23 | }) 24 | } 25 | 26 | export const loginWithToken = () => (dispatch, getState) => { 27 | const token = getState().user.token 28 | 29 | if (typeof token === 'undefined') return 30 | 31 | dispatch({ type: types.LOGIN_REQUEST }) 32 | post({ 33 | url: urls.LOGIN_WITH_TOKEN, 34 | body: { token }, 35 | success: types.LOGIN_SUCCESS, 36 | failure: types.LOGIN_FAILURE, 37 | dispatch, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { Route, Redirect, withRouter, Switch } from 'react-router-dom' 5 | import Async from 'react-code-splitting' 6 | 7 | import Login from './Auth/Login' 8 | import Signup from './Auth/Signup' 9 | import Header from './Header' 10 | import { Body } from './Styled' 11 | 12 | const Home = () => 13 | 14 | const App = ({ user }) => ( 15 | 16 |
17 | 18 | {user.token && } 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | App.propTypes = { 27 | user: PropTypes.shape({}).isRequired, 28 | } 29 | 30 | export default withRouter(connect(state => ({ user: state.user }))(App)) 31 | -------------------------------------------------------------------------------- /src/components/Auth/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { TextField, Submit } from '../Styled' 5 | 6 | const Form = ({ onSubmit }) => ( 7 |
8 | 15 | 23 | 24 | 25 | ) 26 | 27 | Form.propTypes = { 28 | onSubmit: PropTypes.func.isRequired, 29 | } 30 | 31 | export default Form 32 | -------------------------------------------------------------------------------- /src/components/Auth/Login.js: -------------------------------------------------------------------------------- 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 | 6 | import { login } from '../../actions' 7 | 8 | import { FormTitle, FooterLink } from '../Styled' 9 | import Form from './Form' 10 | 11 | const Login = ({ user, login }) => { 12 | const handleSubmit = e => { 13 | e.preventDefault() 14 | const { email: { value: email }, password: { value: password } } = e.target 15 | login({ email, password }) 16 | } 17 | 18 | return ( 19 |
20 | Login 21 |
22 | {"You don't have an account ?"} 23 | {user.token && } 24 |
25 | ) 26 | } 27 | 28 | Login.propTypes = { 29 | user: PropTypes.shape({}).isRequired, 30 | login: PropTypes.func.isRequired, 31 | } 32 | 33 | const mapStateToProps = state => ({ user: state.user }) 34 | export default connect(mapStateToProps, { login })(Login) 35 | -------------------------------------------------------------------------------- /src/components/Auth/Signup.js: -------------------------------------------------------------------------------- 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 | 6 | import { signup } from '../../actions' 7 | 8 | import { FormTitle, FooterLink } from '../Styled' 9 | import Form from './Form' 10 | 11 | const Signup = ({ user, signup }) => { 12 | const handleSubmit = e => { 13 | e.preventDefault() 14 | const { email: { value: email }, password: { value: password } } = e.target 15 | signup({ email, password }) 16 | } 17 | 18 | return ( 19 |
20 | Sign up 21 | 22 | Already have an account ? 23 | {user.token && } 24 |
25 | ) 26 | } 27 | 28 | Signup.propTypes = { 29 | user: PropTypes.shape({}).isRequired, 30 | signup: PropTypes.func.isRequired, 31 | } 32 | 33 | const mapStateToProps = state => ({ user: state.user }) 34 | export default connect(mapStateToProps, { signup })(Signup) 35 | -------------------------------------------------------------------------------- /src/components/Header/Github.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { GithubButton, GithubCount, GithubLink } from '../Styled' 4 | 5 | class Github extends React.Component { 6 | state = { 7 | count: 0, 8 | } 9 | 10 | componentDidMount = async () => { 11 | const url = 'https://api.github.com/repos/didierfranc/redux-react-starter' 12 | const res = await fetch(url).then(r => r.json()) 13 | this.setState({ count: res.stargazers_count }) 14 | } 15 | 16 | render = () => ( 17 | 22 | 23 | Star 24 | 25 | {this.state.count} 26 | 27 | ) 28 | } 29 | 30 | const Star = () => ( 31 | 32 | 33 | 34 | ) 35 | 36 | export default Github 37 | -------------------------------------------------------------------------------- /src/components/Header/__test__/Github.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | 4 | import Github from '../Github' 5 | 6 | global.fetch = jest.fn( 7 | () => 8 | new Promise(resolve => { 9 | process.nextTick(() => resolve({ json: () => ({}) })) 10 | }), 11 | ) 12 | 13 | it('Properly render Github component', () => { 14 | const component = renderer.create() 15 | const tree = component.toJSON() 16 | expect(tree).toMatchSnapshot() 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/Header/__test__/__snapshots__/Github.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Properly render Github component 1`] = ` 4 | 10 | 13 | 23 | 26 | 27 | Star 28 | 29 | 32 | 0 33 | 34 | 35 | `; 36 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Title } from '../Styled' 4 | import Github from './Github' 5 | 6 | const Header = () => ( 7 |
8 | redux-react-starter 9 | 10 |
11 | ) 12 | 13 | export default Header 14 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 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 | 6 | import { Message, Blue } from './Styled' 7 | 8 | const Home = ({ user }) => 9 | user.token ? ( 10 | 11 | {"You're logged in as "} 12 | {user.email} 13 | 14 | ) : ( 15 | 16 | ) 17 | 18 | Home.propTypes = { 19 | user: PropTypes.shape({}).isRequired, 20 | } 21 | 22 | export default connect(state => ({ user: state.user }))(Home) 23 | -------------------------------------------------------------------------------- /src/components/Styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'emotion/react' 2 | import { Link } from 'react-router-dom' 3 | 4 | export const Body = styled.div` 5 | text-align: center; 6 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 7 | ` 8 | 9 | export const Title = styled.h1` 10 | font-family: sans-serif; 11 | font-weight: 100; 12 | margin: 30px 30px 20px 30px; 13 | ` 14 | 15 | const Github = styled.span` 16 | vertical-align: middle; 17 | padding: 6px 10px; 18 | border: 1px solid rgb(213, 213, 213); 19 | font-size: 14px; 20 | font-weight: 400; 21 | outline: none; 22 | font-family: sans-serif; 23 | ` 24 | 25 | export const GithubButton = styled(Github)` 26 | border-radius: 3px 0 0 3px; 27 | background: rgb(248, 248, 248); 28 | &:hover { 29 | background: rgb(238, 238, 238); 30 | } 31 | ` 32 | 33 | export const GithubCount = styled(Github)` 34 | margin-left: -1px; 35 | border-radius: 0 3px 3px 0; 36 | width: 100px; 37 | ` 38 | 39 | export const GithubLink = styled.a` 40 | display: block; 41 | text-decoration: none; 42 | color: black; 43 | ` 44 | 45 | export const Message = styled.h2` 46 | font-family: sans-serif; 47 | font-weight: 100; 48 | margin-top: 30vh; 49 | ` 50 | 51 | export const Blue = styled.span`color: rgb(0, 128, 255);` 52 | 53 | export const FormTitle = styled.h1` 54 | font-family: sans-serif; 55 | font-weight: 100; 56 | margin-top: 22vh; 57 | margin-bottom: 50px; 58 | @media (max-width: 500px) { 59 | margin-top: 15vh; 60 | } 61 | ` 62 | 63 | export const TextField = styled.input` 64 | display: block; 65 | height: 42px; 66 | width: 300px; 67 | margin: 10px auto; 68 | padding: 0 12px; 69 | border-radius: 3px; 70 | border: 1px solid lightgrey; 71 | outline: none; 72 | font-size: 17px; 73 | box-sizing: border-box; 74 | appearance: none; 75 | &:focus { 76 | border-color: rgb(0, 128, 255); 77 | } 78 | ` 79 | 80 | export const Submit = styled.input` 81 | border: none; 82 | color: rgb(0, 128, 255); 83 | font-size: 24px; 84 | background: none; 85 | outline: none; 86 | cursor: pointer; 87 | margin-top: 30px; 88 | ` 89 | 90 | export const FooterLink = styled(Link)` 91 | position: fixed; 92 | left: 0; 93 | bottom: 15px; 94 | width: 100%; 95 | font-size: 14px; 96 | font-family: sans-serif; 97 | font-weight: 100; 98 | text-decoration: none; 99 | color: rgb(10, 10, 10); 100 | &:hover { 101 | color: rgb(0, 0, 0); 102 | } 103 | ` 104 | -------------------------------------------------------------------------------- /src/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const SIGNUP_REQUEST = 'SIGNUP_REQUEST' 2 | export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS' 3 | export const SIGNUP_FAILURE = 'SIGNUP_FAILURE' 4 | 5 | export const LOGIN_REQUEST = 'LOGIN_REQUEST' 6 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 7 | export const LOGIN_FAILURE = 'LOGIN_FAILURE' 8 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export * as urls from './urls' 2 | export * as actionTypes from './actionTypes' 3 | -------------------------------------------------------------------------------- /src/constants/urls.js: -------------------------------------------------------------------------------- 1 | const API = 'https://react.didierfranc.com' 2 | 3 | export const SIGNUP = `${API}/signup` 4 | export const LOGIN = `${API}/login` 5 | export const LOGIN_WITH_TOKEN = `${API}/token` 6 | -------------------------------------------------------------------------------- /src/helpers/fetch.js: -------------------------------------------------------------------------------- 1 | export const post = async ({ url, body, success, failure, dispatch }) => { 2 | try { 3 | const res = await fetch(url, { 4 | method: 'POST', 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | body: JSON.stringify(body), 9 | }) 10 | const data = await res.json() 11 | dispatch({ type: success, data }) 12 | } catch (e) { 13 | dispatch({ type: failure }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export * from './persist' 2 | export * from './fetch' 3 | -------------------------------------------------------------------------------- /src/helpers/persist.js: -------------------------------------------------------------------------------- 1 | export const saveState = state => { 2 | try { 3 | const serializedState = JSON.stringify(state) 4 | localStorage.setItem('state', serializedState) 5 | } catch (err) { 6 | console.warn(err) 7 | } 8 | } 9 | 10 | export const loadState = () => { 11 | try { 12 | const serializedState = localStorage.getItem('state') 13 | if (serializedState === null) { 14 | return undefined 15 | } 16 | return JSON.parse(serializedState) 17 | } catch (err) { 18 | return undefined 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Offline from 'offline-plugin/runtime' 2 | import React from 'react' 3 | import { Provider } from 'react-redux' 4 | import { render } from 'react-dom' 5 | import { BrowserRouter } from 'react-router-dom' 6 | 7 | import { store } from './store' 8 | 9 | import App from './components/App' 10 | 11 | if (process.env.NODE_ENV === 'production') Offline.install() 12 | 13 | export const Root = () => ( 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | 21 | if (!module.hot) render(, document.querySelector('react')) 22 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import user from './user' 4 | 5 | const rootReducer = combineReducers({ 6 | user, 7 | }) 8 | 9 | export default rootReducer 10 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { actionTypes as types } from '../constants' 2 | 3 | const user = (state = {}, action) => { 4 | switch (action.type) { 5 | case types.SIGNUP_SUCCESS: 6 | case types.LOGIN_SUCCESS: 7 | return action.data 8 | case types.LOGIN_FAILURE: 9 | return {} 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | export default user 16 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import throttle from 'lodash/throttle' 4 | 5 | import rootReducer from './reducers' 6 | import { loginWithToken } from './actions' 7 | import { saveState, loadState } from './helpers' 8 | 9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 10 | 11 | export const store = createStore( 12 | rootReducer, 13 | loadState(), 14 | composeEnhancers(applyMiddleware(thunk)), 15 | ) 16 | 17 | store.subscribe(throttle(() => saveState(store.getState()), 1000)) 18 | store.dispatch(loginWithToken()) 19 | -------------------------------------------------------------------------------- /webpack/hotReload.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import { Root } from '../src' 5 | 6 | const render = () => { 7 | ReactDOM.render(, document.querySelector('react')) 8 | } 9 | 10 | render() 11 | 12 | module.hot.accept('../src', render) 13 | -------------------------------------------------------------------------------- /webpack/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const webpack = require('webpack') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: [ 7 | 'webpack-dev-server/client', 8 | 'webpack/hot/only-dev-server', 9 | resolve(__dirname, 'hotReload'), 10 | ], 11 | output: { 12 | filename: 'bundle.js', 13 | path: resolve(__dirname), 14 | publicPath: '/', 15 | }, 16 | context: resolve(__dirname, '../src'), 17 | devtool: 'inline-source-map', 18 | devServer: { 19 | hot: true, 20 | host: '0.0.0.0', 21 | contentBase: resolve(__dirname, '../assets'), 22 | publicPath: '/', 23 | historyApiFallback: true, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | include: [resolve(__dirname, '../src'), resolve(__dirname)], 30 | use: 'babel-loader', 31 | }, 32 | ], 33 | }, 34 | plugins: [ 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.NamedModulesPlugin(), 37 | new HtmlWebpackPlugin({ 38 | title: 'redux-react-starter', 39 | template: '../webpack/template.html', 40 | }), 41 | ], 42 | performance: { hints: false }, 43 | } 44 | -------------------------------------------------------------------------------- /webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const webpack = require('webpack') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const OfflinePlugin = require('offline-plugin') 5 | const PreloadWebpackPlugin = require('preload-webpack-plugin') 6 | 7 | module.exports = { 8 | entry: { 9 | main: resolve(__dirname, '../src'), 10 | vendor: [ 11 | 'react', 12 | 'react-dom', 13 | 'react-redux', 14 | 'react-router-dom', 15 | 'redux', 16 | 'redux-thunk', 17 | 'emotion', 18 | ], 19 | }, 20 | output: { 21 | filename: '[name].[chunkhash].js', 22 | path: resolve(__dirname, '../dist'), 23 | publicPath: '/', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | include: [resolve(__dirname, '../src')], 30 | use: 'babel-loader', 31 | }, 32 | ], 33 | }, 34 | plugins: [ 35 | new webpack.optimize.ModuleConcatenationPlugin(), 36 | new webpack.DefinePlugin({ 37 | 'process.env': { 38 | NODE_ENV: JSON.stringify('production'), 39 | }, 40 | }), 41 | new webpack.optimize.UglifyJsPlugin(), 42 | new webpack.optimize.CommonsChunkPlugin({ 43 | names: ['vendor', 'manifest'], 44 | }), 45 | new HtmlWebpackPlugin({ 46 | filename: 'index.html', 47 | title: 'redux-react-starter', 48 | template: 'webpack/template.html', 49 | }), 50 | new PreloadWebpackPlugin({ 51 | rel: 'preload', 52 | as: 'script', 53 | include: 'all', 54 | }), 55 | new OfflinePlugin({ 56 | ServiceWorker: { 57 | navigateFallbackURL: '/', 58 | }, 59 | AppCache: false, 60 | }), 61 | ], 62 | } 63 | --------------------------------------------------------------------------------