├── .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 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------