├── .gitignore
├── README.md
├── config
├── babel.dev.js
├── babel.prod.js
├── eslint.js
├── flow
│ ├── css.js.flow
│ └── file.js.flow
├── paths.js
├── polyfills.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── favicon.ico
├── images
└── demo.gif
├── index.html
├── package.json
├── scripts
├── build.js
├── start.js
└── utils
│ ├── chrome.applescript
│ ├── detectPort.js
│ └── prompt.js
└── src
├── App.css
├── App.js
├── actions
├── action-types.js
└── people-actions.js
├── components
├── PeopleContainer.js
├── PeopleList.js
├── Person.js
└── PersonInput.js
├── index.css
├── index.js
├── logo.svg
├── reducers
├── index.js
└── people-reducer.js
└── store
└── configure-store.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # production
7 | build
8 |
9 | # misc
10 | .DS_Store
11 | npm-debug.log
12 |
13 | # typings definitions (used with VS Code)
14 | typings
15 |
16 | # jsconfig for intellisense in VS Code
17 | jsconfig.json
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `create-react-app` ... with Redux
2 |
3 | This is a barebones implementation of Redux with a React app that was generated with `create-react-app`, and then ejected with `npm run eject`.
4 |
5 | :bulb: This repository consists of two commits and only two commits.
6 |
7 | 1. **first commit** is the base code of a `create-react-app` app source code after eject (`npm run eject`)
8 | 2. **second commit** are the sample additions to implement basic redux
9 |
10 | :bulb: :bulb: It is worth noting that it is **not** required to run `npm run eject` in order to get Redux implemented in this app. Implementing Redux in the app without ejecting is completely possible
11 |
12 | 
13 |
--------------------------------------------------------------------------------
/config/babel.dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | babelrc: false,
3 | cacheDirectory: true,
4 | presets: [
5 | 'babel-preset-es2015',
6 | 'babel-preset-es2016',
7 | 'babel-preset-react'
8 | ].map(require.resolve),
9 | plugins: [
10 | 'babel-plugin-syntax-trailing-function-commas',
11 | 'babel-plugin-transform-class-properties',
12 | 'babel-plugin-transform-object-rest-spread'
13 | ].map(require.resolve).concat([
14 | [require.resolve('babel-plugin-transform-runtime'), {
15 | helpers: false,
16 | polyfill: false,
17 | regenerator: true
18 | }]
19 | ])
20 | };
21 |
--------------------------------------------------------------------------------
/config/babel.prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | babelrc: false,
3 | presets: [
4 | 'babel-preset-es2015',
5 | 'babel-preset-es2016',
6 | 'babel-preset-react'
7 | ].map(require.resolve),
8 | plugins: [
9 | 'babel-plugin-syntax-trailing-function-commas',
10 | 'babel-plugin-transform-class-properties',
11 | 'babel-plugin-transform-object-rest-spread',
12 | 'babel-plugin-transform-react-constant-elements',
13 | ].map(require.resolve).concat([
14 | [require.resolve('babel-plugin-transform-runtime'), {
15 | helpers: false,
16 | polyfill: false,
17 | regenerator: true
18 | }]
19 | ])
20 | };
21 |
--------------------------------------------------------------------------------
/config/eslint.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/airbnb/javascript but less opinionated.
2 |
3 | // We use eslint-loader so even warnings are very visibile.
4 | // This is why we only use "WARNING" level for potential errors,
5 | // and we don't use "ERROR" level at all.
6 |
7 | // In the future, we might create a separate list of rules for production.
8 | // It would probably be more strict.
9 |
10 | module.exports = {
11 | root: true,
12 |
13 | parser: 'babel-eslint',
14 |
15 | // import plugin is termporarily disabled, scroll below to see why
16 | plugins: [/*'import', */'flowtype', 'jsx-a11y', 'react'],
17 |
18 | env: {
19 | browser: true,
20 | commonjs: true,
21 | es6: true,
22 | node: true
23 | },
24 |
25 | parserOptions: {
26 | ecmaVersion: 6,
27 | sourceType: 'module',
28 | ecmaFeatures: {
29 | jsx: true,
30 | generators: true,
31 | experimentalObjectRestSpread: true
32 | }
33 | },
34 |
35 | settings: {
36 | 'import/ignore': [
37 | 'node_modules',
38 | '\\.(json|css|jpg|png|gif|eot|svg|ttf|woff|woff2|mp4|webm)$',
39 | ],
40 | 'import/extensions': ['.js'],
41 | 'import/resolver': {
42 | node: {
43 | extensions: ['.js', '.json']
44 | }
45 | }
46 | },
47 |
48 | rules: {
49 | // http://eslint.org/docs/rules/
50 | 'array-callback-return': 'warn',
51 | 'default-case': ['warn', { commentPattern: '^no default$' }],
52 | 'dot-location': ['warn', 'property'],
53 | eqeqeq: ['warn', 'allow-null'],
54 | 'guard-for-in': 'warn',
55 | 'new-cap': ['warn', { newIsCap: true }],
56 | 'new-parens': 'warn',
57 | 'no-array-constructor': 'warn',
58 | 'no-caller': 'warn',
59 | 'no-cond-assign': ['warn', 'always'],
60 | 'no-const-assign': 'warn',
61 | 'no-control-regex': 'warn',
62 | 'no-delete-var': 'warn',
63 | 'no-dupe-args': 'warn',
64 | 'no-dupe-class-members': 'warn',
65 | 'no-dupe-keys': 'warn',
66 | 'no-duplicate-case': 'warn',
67 | 'no-empty-character-class': 'warn',
68 | 'no-empty-pattern': 'warn',
69 | 'no-eval': 'warn',
70 | 'no-ex-assign': 'warn',
71 | 'no-extend-native': 'warn',
72 | 'no-extra-bind': 'warn',
73 | 'no-extra-label': 'warn',
74 | 'no-fallthrough': 'warn',
75 | 'no-func-assign': 'warn',
76 | 'no-implied-eval': 'warn',
77 | 'no-invalid-regexp': 'warn',
78 | 'no-iterator': 'warn',
79 | 'no-label-var': 'warn',
80 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }],
81 | 'no-lone-blocks': 'warn',
82 | 'no-loop-func': 'warn',
83 | 'no-mixed-operators': ['warn', {
84 | groups: [
85 | ['&', '|', '^', '~', '<<', '>>', '>>>'],
86 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='],
87 | ['&&', '||'],
88 | ['in', 'instanceof']
89 | ],
90 | allowSamePrecedence: false
91 | }],
92 | 'no-multi-str': 'warn',
93 | 'no-native-reassign': 'warn',
94 | 'no-negated-in-lhs': 'warn',
95 | 'no-new-func': 'warn',
96 | 'no-new-object': 'warn',
97 | 'no-new-symbol': 'warn',
98 | 'no-new-wrappers': 'warn',
99 | 'no-obj-calls': 'warn',
100 | 'no-octal': 'warn',
101 | 'no-octal-escape': 'warn',
102 | 'no-redeclare': 'warn',
103 | 'no-regex-spaces': 'warn',
104 | 'no-restricted-syntax': [
105 | 'warn',
106 | 'LabeledStatement',
107 | 'WithStatement',
108 | ],
109 | 'no-return-assign': 'warn',
110 | 'no-script-url': 'warn',
111 | 'no-self-assign': 'warn',
112 | 'no-self-compare': 'warn',
113 | 'no-sequences': 'warn',
114 | 'no-shadow-restricted-names': 'warn',
115 | 'no-sparse-arrays': 'warn',
116 | 'no-this-before-super': 'warn',
117 | 'no-throw-literal': 'warn',
118 | 'no-undef': 'warn',
119 | 'no-unexpected-multiline': 'warn',
120 | 'no-unreachable': 'warn',
121 | 'no-unused-expressions': 'warn',
122 | 'no-unused-labels': 'warn',
123 | 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }],
124 | 'no-use-before-define': ['warn', 'nofunc'],
125 | 'no-useless-computed-key': 'warn',
126 | 'no-useless-concat': 'warn',
127 | 'no-useless-constructor': 'warn',
128 | 'no-useless-escape': 'warn',
129 | 'no-useless-rename': ['warn', {
130 | ignoreDestructuring: false,
131 | ignoreImport: false,
132 | ignoreExport: false,
133 | }],
134 | 'no-with': 'warn',
135 | 'no-whitespace-before-property': 'warn',
136 | 'operator-assignment': ['warn', 'always'],
137 | radix: 'warn',
138 | 'require-yield': 'warn',
139 | 'rest-spread-spacing': ['warn', 'never'],
140 | strict: ['warn', 'never'],
141 | 'unicode-bom': ['warn', 'never'],
142 | 'use-isnan': 'warn',
143 | 'valid-typeof': 'warn',
144 |
145 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/
146 |
147 | // TODO: import rules are temporarily disabled because they don't play well
148 | // with how eslint-loader only checks the file you change. So if module A
149 | // imports module B, and B is missing a default export, the linter will
150 | // record this as an issue in module A. Now if you fix module B, the linter
151 | // will not be aware that it needs to re-lint A as well, so the error
152 | // will stay until the next restart, which is really confusing.
153 |
154 | // This is probably fixable with a patch to eslint-loader.
155 | // When file A is saved, we want to invalidate all files that import it
156 | // *and* that currently have lint errors. This should fix the problem.
157 |
158 | // 'import/default': 'warn',
159 | // 'import/export': 'warn',
160 | // 'import/named': 'warn',
161 | // 'import/namespace': 'warn',
162 | // 'import/no-amd': 'warn',
163 | // 'import/no-duplicates': 'warn',
164 | // 'import/no-extraneous-dependencies': 'warn',
165 | // 'import/no-named-as-default': 'warn',
166 | // 'import/no-named-as-default-member': 'warn',
167 | // 'import/no-unresolved': ['warn', { commonjs: true }],
168 |
169 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
170 | 'react/jsx-equals-spacing': ['warn', 'never'],
171 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }],
172 | 'react/jsx-no-undef': 'warn',
173 | 'react/jsx-pascal-case': ['warn', {
174 | allowAllCaps: true,
175 | ignore: [],
176 | }],
177 | 'react/jsx-uses-react': 'warn',
178 | 'react/jsx-uses-vars': 'warn',
179 | 'react/no-deprecated': 'warn',
180 | 'react/no-direct-mutation-state': 'warn',
181 | 'react/no-is-mounted': 'warn',
182 | 'react/react-in-jsx-scope': 'warn',
183 | 'react/require-render-return': 'warn',
184 |
185 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
186 | 'jsx-a11y/aria-role': 'warn',
187 | 'jsx-a11y/img-has-alt': 'warn',
188 | 'jsx-a11y/img-redundant-alt': 'warn',
189 | 'jsx-a11y/no-access-key': 'warn',
190 |
191 | // https://github.com/gajus/eslint-plugin-flowtype
192 | 'flowtype/define-flow-type': 'warn',
193 | 'flowtype/require-valid-file-annotation': 'warn',
194 | 'flowtype/use-flow-type': 'warn'
195 | }
196 | };
197 |
--------------------------------------------------------------------------------
/config/flow/css.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 |
--------------------------------------------------------------------------------
/config/flow/file.js.flow:
--------------------------------------------------------------------------------
1 | // @flow
2 | declare export default string;
3 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | // TODO: we can split this file into several files (pre-eject, post-eject, test)
2 | // and use those instead. This way we don't need to branch here.
3 |
4 | var path = require('path');
5 |
6 | // True after ejecting, false when used as a dependency
7 | var isEjected = (
8 | path.resolve(path.join(__dirname, '..')) ===
9 | path.resolve(process.cwd())
10 | );
11 |
12 | // Are we developing create-react-app locally?
13 | var isInCreateReactAppSource = (
14 | process.argv.some(arg => arg.indexOf('--debug-template') > -1)
15 | );
16 |
17 | function resolveOwn(relativePath) {
18 | return path.resolve(__dirname, relativePath);
19 | }
20 |
21 | function resolveApp(relativePath) {
22 | return path.resolve(relativePath);
23 | }
24 |
25 | if (isInCreateReactAppSource) {
26 | // create-react-app development: we're in ./config/
27 | module.exports = {
28 | appBuild: resolveOwn('../build'),
29 | appHtml: resolveOwn('../template/index.html'),
30 | appFavicon: resolveOwn('../template/favicon.ico'),
31 | appPackageJson: resolveOwn('../package.json'),
32 | appSrc: resolveOwn('../template/src'),
33 | appNodeModules: resolveOwn('../node_modules'),
34 | ownNodeModules: resolveOwn('../node_modules')
35 | };
36 | } else if (!isEjected) {
37 | // before eject: we're in ./node_modules/react-scripts/config/
38 | module.exports = {
39 | appBuild: resolveApp('build'),
40 | appHtml: resolveApp('index.html'),
41 | appFavicon: resolveApp('favicon.ico'),
42 | appPackageJson: resolveApp('package.json'),
43 | appSrc: resolveApp('src'),
44 | appNodeModules: resolveApp('node_modules'),
45 | // this is empty with npm3 but node resolution searches higher anyway:
46 | ownNodeModules: resolveOwn('../node_modules')
47 | };
48 | } else {
49 | // after eject: we're in ./config/
50 | module.exports = {
51 | appBuild: resolveApp('build'),
52 | appHtml: resolveApp('index.html'),
53 | appFavicon: resolveApp('favicon.ico'),
54 | appPackageJson: resolveApp('package.json'),
55 | appSrc: resolveApp('src'),
56 | appNodeModules: resolveApp('node_modules'),
57 | ownNodeModules: resolveApp('node_modules')
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/config/polyfills.js:
--------------------------------------------------------------------------------
1 | if (typeof Promise === 'undefined') {
2 | // Rejection tracking prevents a common issue where React gets into an
3 | // inconsistent state due to an error, but it gets swallowed by a Promise,
4 | // and the user has no idea what causes React's erratic future behavior.
5 | require('promise/lib/rejection-tracking').enable();
6 | window.Promise = require('promise/lib/es6-extensions.js');
7 | }
8 |
9 | require('whatwg-fetch');
10 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var autoprefixer = require('autoprefixer');
3 | var webpack = require('webpack');
4 | var HtmlWebpackPlugin = require('html-webpack-plugin');
5 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
6 | var paths = require('./paths');
7 |
8 | module.exports = {
9 | devtool: 'eval',
10 | entry: [
11 | require.resolve('webpack-dev-server/client') + '?/',
12 | require.resolve('webpack/hot/dev-server'),
13 | require.resolve('./polyfills'),
14 | path.join(paths.appSrc, 'index')
15 | ],
16 | output: {
17 | // Next line is not used in dev but WebpackDevServer crashes without it:
18 | path: paths.appBuild,
19 | pathinfo: true,
20 | filename: 'static/js/bundle.js',
21 | publicPath: '/'
22 | },
23 | resolve: {
24 | extensions: ['', '.js', '.json'],
25 | alias: {
26 | // This `alias` section can be safely removed after ejection.
27 | // We do this because `babel-runtime` may be inside `react-scripts`,
28 | // so when `babel-plugin-transform-runtime` imports it, it will not be
29 | // available to the app directly. This is a temporary solution that lets
30 | // us ship support for generators. However it is far from ideal, and
31 | // if we don't have a good solution, we should just make `babel-runtime`
32 | // a dependency in generated projects.
33 | // See https://github.com/facebookincubator/create-react-app/issues/255
34 | 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator')
35 | }
36 | },
37 | resolveLoader: {
38 | root: paths.ownNodeModules,
39 | moduleTemplates: ['*-loader']
40 | },
41 | module: {
42 | preLoaders: [
43 | {
44 | test: /\.js$/,
45 | loader: 'eslint',
46 | include: paths.appSrc,
47 | }
48 | ],
49 | loaders: [
50 | {
51 | test: /\.js$/,
52 | include: paths.appSrc,
53 | loader: 'babel',
54 | query: require('./babel.dev')
55 | },
56 | {
57 | test: /\.css$/,
58 | include: [paths.appSrc, paths.appNodeModules],
59 | loader: 'style!css!postcss'
60 | },
61 | {
62 | test: /\.json$/,
63 | include: [paths.appSrc, paths.appNodeModules],
64 | loader: 'json'
65 | },
66 | {
67 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
68 | include: [paths.appSrc, paths.appNodeModules],
69 | loader: 'file',
70 | query: {
71 | name: 'static/media/[name].[ext]'
72 | }
73 | },
74 | {
75 | test: /\.(mp4|webm)(\?.*)?$/,
76 | include: [paths.appSrc, paths.appNodeModules],
77 | loader: 'url',
78 | query: {
79 | limit: 10000,
80 | name: 'static/media/[name].[ext]'
81 | }
82 | }
83 | ]
84 | },
85 | eslint: {
86 | configFile: path.join(__dirname, 'eslint.js'),
87 | useEslintrc: false
88 | },
89 | postcss: function() {
90 | return [autoprefixer];
91 | },
92 | plugins: [
93 | new HtmlWebpackPlugin({
94 | inject: true,
95 | template: paths.appHtml,
96 | favicon: paths.appFavicon,
97 | }),
98 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }),
99 | // Note: only CSS is currently hot reloaded
100 | new webpack.HotModuleReplacementPlugin(),
101 | new CaseSensitivePathsPlugin()
102 | ]
103 | };
104 |
--------------------------------------------------------------------------------
/config/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var autoprefixer = require('autoprefixer');
3 | var webpack = require('webpack');
4 | var HtmlWebpackPlugin = require('html-webpack-plugin');
5 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
6 | var url = require('url');
7 | var paths = require('./paths');
8 |
9 | var homepagePath = require(paths.appPackageJson).homepage;
10 | var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/';
11 | if (!publicPath.endsWith('/')) {
12 | // Prevents incorrect paths in file-loader
13 | publicPath += '/';
14 | }
15 |
16 | module.exports = {
17 | bail: true,
18 | devtool: 'source-map',
19 | entry: [
20 | require.resolve('./polyfills'),
21 | path.join(paths.appSrc, 'index')
22 | ],
23 | output: {
24 | path: paths.appBuild,
25 | filename: 'static/js/[name].[chunkhash:8].js',
26 | chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
27 | publicPath: publicPath
28 | },
29 | resolve: {
30 | extensions: ['', '.js', '.json'],
31 | alias: {
32 | // This `alias` section can be safely removed after ejection.
33 | // We do this because `babel-runtime` may be inside `react-scripts`,
34 | // so when `babel-plugin-transform-runtime` imports it, it will not be
35 | // available to the app directly. This is a temporary solution that lets
36 | // us ship support for generators. However it is far from ideal, and
37 | // if we don't have a good solution, we should just make `babel-runtime`
38 | // a dependency in generated projects.
39 | // See https://github.com/facebookincubator/create-react-app/issues/255
40 | 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator')
41 | }
42 | },
43 | resolveLoader: {
44 | root: paths.ownNodeModules,
45 | moduleTemplates: ['*-loader']
46 | },
47 | module: {
48 | preLoaders: [
49 | {
50 | test: /\.js$/,
51 | loader: 'eslint',
52 | include: paths.appSrc
53 | }
54 | ],
55 | loaders: [
56 | {
57 | test: /\.js$/,
58 | include: paths.appSrc,
59 | loader: 'babel',
60 | query: require('./babel.prod')
61 | },
62 | {
63 | test: /\.css$/,
64 | include: [paths.appSrc, paths.appNodeModules],
65 | // Disable autoprefixer in css-loader itself:
66 | // https://github.com/webpack/css-loader/issues/281
67 | // We already have it thanks to postcss.
68 | loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss')
69 | },
70 | {
71 | test: /\.json$/,
72 | include: [paths.appSrc, paths.appNodeModules],
73 | loader: 'json'
74 | },
75 | {
76 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
77 | include: [paths.appSrc, paths.appNodeModules],
78 | loader: 'file',
79 | query: {
80 | name: 'static/media/[name].[hash:8].[ext]'
81 | }
82 | },
83 | {
84 | test: /\.(mp4|webm)(\?.*)?$/,
85 | include: [paths.appSrc, paths.appNodeModules],
86 | loader: 'url',
87 | query: {
88 | limit: 10000,
89 | name: 'static/media/[name].[hash:8].[ext]'
90 | }
91 | }
92 | ]
93 | },
94 | eslint: {
95 | // TODO: consider separate config for production,
96 | // e.g. to enable no-console and no-debugger only in prod.
97 | configFile: path.join(__dirname, 'eslint.js'),
98 | useEslintrc: false
99 | },
100 | postcss: function() {
101 | return [autoprefixer];
102 | },
103 | plugins: [
104 | new HtmlWebpackPlugin({
105 | inject: true,
106 | template: paths.appHtml,
107 | favicon: paths.appFavicon,
108 | minify: {
109 | removeComments: true,
110 | collapseWhitespace: true,
111 | removeRedundantAttributes: true,
112 | useShortDoctype: true,
113 | removeEmptyAttributes: true,
114 | removeStyleLinkTypeAttributes: true,
115 | keepClosingSlash: true,
116 | minifyJS: true,
117 | minifyCSS: true,
118 | minifyURLs: true
119 | }
120 | }),
121 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }),
122 | new webpack.optimize.OccurrenceOrderPlugin(),
123 | new webpack.optimize.DedupePlugin(),
124 | new webpack.optimize.UglifyJsPlugin({
125 | compress: {
126 | screw_ie8: true,
127 | warnings: false
128 | },
129 | mangle: {
130 | screw_ie8: true
131 | },
132 | output: {
133 | comments: false,
134 | screw_ie8: true
135 | }
136 | }),
137 | new ExtractTextPlugin('static/css/[name].[contenthash:8].css')
138 | ]
139 | };
140 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trstringer/create-react-app-with-redux/1966b89cbe609ce3794760ea41111252d1e70a8a/favicon.ico
--------------------------------------------------------------------------------
/images/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/trstringer/create-react-app-with-redux/1966b89cbe609ce3794760ea41111252d1e70a8a/images/demo.gif
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React App
7 |
8 |
9 |
10 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-react-app-with-redux",
3 | "version": "0.0.1",
4 | "private": true,
5 | "devDependencies": {
6 | "autoprefixer": "6.3.7",
7 | "babel-core": "6.11.4",
8 | "babel-eslint": "6.1.2",
9 | "babel-loader": "6.2.4",
10 | "babel-plugin-syntax-trailing-function-commas": "6.8.0",
11 | "babel-plugin-transform-class-properties": "6.11.5",
12 | "babel-plugin-transform-object-rest-spread": "6.8.0",
13 | "babel-plugin-transform-react-constant-elements": "6.9.1",
14 | "babel-plugin-transform-runtime": "6.12.0",
15 | "babel-preset-es2015": "6.9.0",
16 | "babel-preset-es2016": "6.11.3",
17 | "babel-preset-react": "6.11.1",
18 | "babel-runtime": "6.11.6",
19 | "case-sensitive-paths-webpack-plugin": "1.1.2",
20 | "chalk": "1.1.3",
21 | "cross-spawn": "4.0.0",
22 | "css-loader": "0.23.1",
23 | "eslint": "3.1.1",
24 | "eslint-loader": "1.4.1",
25 | "eslint-plugin-flowtype": "2.4.0",
26 | "eslint-plugin-import": "1.12.0",
27 | "eslint-plugin-jsx-a11y": "2.0.1",
28 | "eslint-plugin-react": "5.2.2",
29 | "extract-text-webpack-plugin": "1.0.1",
30 | "file-loader": "0.9.0",
31 | "filesize": "3.3.0",
32 | "fs-extra": "0.30.0",
33 | "gzip-size": "3.0.0",
34 | "html-webpack-plugin": "2.22.0",
35 | "json-loader": "0.5.4",
36 | "opn": "4.0.2",
37 | "postcss-loader": "0.9.1",
38 | "promise": "7.1.1",
39 | "rimraf": "2.5.4",
40 | "style-loader": "0.13.1",
41 | "url-loader": "0.5.7",
42 | "webpack": "1.13.1",
43 | "webpack-dev-server": "1.14.1",
44 | "whatwg-fetch": "1.0.0"
45 | },
46 | "dependencies": {
47 | "react": "^15.2.1",
48 | "react-dom": "^15.2.1",
49 | "react-redux": "^4.4.5",
50 | "redux": "^3.5.2"
51 | },
52 | "scripts": {
53 | "start": "node ./scripts/start.js",
54 | "build": "node ./scripts/build.js"
55 | },
56 | "eslintConfig": {
57 | "extends": "./config/eslint.js"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'production';
2 |
3 | var chalk = require('chalk');
4 | var fs = require('fs');
5 | var path = require('path');
6 | var filesize = require('filesize');
7 | var gzipSize = require('gzip-size').sync;
8 | var rimrafSync = require('rimraf').sync;
9 | var webpack = require('webpack');
10 | var config = require('../config/webpack.config.prod');
11 | var paths = require('../config/paths');
12 |
13 | // Remove all content but keep the directory so that
14 | // if you're in it, you don't end up in Trash
15 | rimrafSync(paths.appBuild + '/*');
16 |
17 | console.log('Creating an optimized production build...');
18 | webpack(config).run(function(err, stats) {
19 | if (err) {
20 | console.error('Failed to create a production build. Reason:');
21 | console.error(err.message || err);
22 | process.exit(1);
23 | }
24 |
25 | console.log(chalk.green('Compiled successfully.'));
26 | console.log();
27 |
28 | console.log('File sizes after gzip:');
29 | console.log();
30 | var assets = stats.toJson().assets
31 | .filter(asset => /\.(js|css)$/.test(asset.name))
32 | .map(asset => {
33 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name);
34 | var size = gzipSize(fileContents);
35 | return {
36 | folder: path.join('build', path.dirname(asset.name)),
37 | name: path.basename(asset.name),
38 | size: size,
39 | sizeLabel: filesize(size)
40 | };
41 | });
42 | assets.sort((a, b) => b.size - a.size);
43 |
44 | var longestSizeLabelLength = Math.max.apply(null,
45 | assets.map(a => a.sizeLabel.length)
46 | );
47 | assets.forEach(asset => {
48 | var sizeLabel = asset.sizeLabel;
49 | if (sizeLabel.length < longestSizeLabelLength) {
50 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLabel.length);
51 | sizeLabel += rightPadding;
52 | }
53 | console.log(
54 | ' ' + chalk.green(sizeLabel) +
55 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)
56 | );
57 | });
58 | console.log();
59 |
60 | var openCommand = process.platform === 'win32' ? 'start' : 'open';
61 | var homepagePath = require(paths.appPackageJson).homepage;
62 | if (homepagePath) {
63 | console.log('You can now publish them at ' + homepagePath + '.');
64 | console.log('For example, if you use GitHub Pages:');
65 | console.log();
66 | console.log(' git commit -am "Save local changes"');
67 | console.log(' git checkout -B gh-pages');
68 | console.log(' git add -f build');
69 | console.log(' git commit -am "Rebuild website"');
70 | console.log(' git filter-branch -f --prune-empty --subdirectory-filter build');
71 | console.log(' git push -f origin gh-pages');
72 | console.log(' git checkout -');
73 | console.log();
74 | } else {
75 | console.log('You can now serve them with any static server.');
76 | console.log('For example:');
77 | console.log();
78 | console.log(' npm install -g pushstate-server');
79 | console.log(' pushstate-server build');
80 | console.log(' ' + openCommand + ' http://localhost:9000');
81 | console.log();
82 | console.log(chalk.dim('The project was built assuming it is hosted at the root.'));
83 | console.log(chalk.dim('Set the "homepage" field in package.json to override this.'));
84 | console.log(chalk.dim('For example, "homepage": "http://user.github.io/project".'));
85 | }
86 | console.log();
87 | });
88 |
--------------------------------------------------------------------------------
/scripts/start.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'development';
2 |
3 | var path = require('path');
4 | var chalk = require('chalk');
5 | var webpack = require('webpack');
6 | var WebpackDevServer = require('webpack-dev-server');
7 | var execSync = require('child_process').execSync;
8 | var opn = require('opn');
9 | var detect = require('./utils/detectPort');
10 | var prompt = require('./utils/prompt');
11 | var config = require('../config/webpack.config.dev');
12 |
13 | // Tools like Cloud9 rely on this
14 | var DEFAULT_PORT = process.env.PORT || 3000;
15 | var compiler;
16 |
17 | // TODO: hide this behind a flag and eliminate dead code on eject.
18 | // This shouldn't be exposed to the user.
19 | var handleCompile;
20 | var isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
21 | if (isSmokeTest) {
22 | handleCompile = function (err, stats) {
23 | if (err || stats.hasErrors() || stats.hasWarnings()) {
24 | process.exit(1);
25 | } else {
26 | process.exit(0);
27 | }
28 | };
29 | }
30 |
31 | var friendlySyntaxErrorLabel = 'Syntax error:';
32 |
33 | function isLikelyASyntaxError(message) {
34 | return message.indexOf(friendlySyntaxErrorLabel) !== -1;
35 | }
36 |
37 | // This is a little hacky.
38 | // It would be easier if webpack provided a rich error object.
39 |
40 | function formatMessage(message) {
41 | return message
42 | // Make some common errors shorter:
43 | .replace(
44 | // Babel syntax error
45 | 'Module build failed: SyntaxError:',
46 | friendlySyntaxErrorLabel
47 | )
48 | .replace(
49 | // Webpack file not found error
50 | /Module not found: Error: Cannot resolve 'file' or 'directory'/,
51 | 'Module not found:'
52 | )
53 | // Internal stacks are generally useless so we strip them
54 | .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y
55 | // Webpack loader names obscure CSS filenames
56 | .replace('./~/css-loader!./~/postcss-loader!', '');
57 | }
58 |
59 | function clearConsole() {
60 | process.stdout.write('\x1bc');
61 | }
62 |
63 | function setupCompiler(port) {
64 | compiler = webpack(config, handleCompile);
65 |
66 | compiler.plugin('invalid', function() {
67 | clearConsole();
68 | console.log('Compiling...');
69 | });
70 |
71 | compiler.plugin('done', function(stats) {
72 | clearConsole();
73 | var hasErrors = stats.hasErrors();
74 | var hasWarnings = stats.hasWarnings();
75 | if (!hasErrors && !hasWarnings) {
76 | console.log(chalk.green('Compiled successfully!'));
77 | console.log();
78 | console.log('The app is running at http://localhost:' + port + '/');
79 | console.log();
80 | return;
81 | }
82 |
83 | var json = stats.toJson();
84 | var formattedErrors = json.errors.map(message =>
85 | 'Error in ' + formatMessage(message)
86 | );
87 | var formattedWarnings = json.warnings.map(message =>
88 | 'Warning in ' + formatMessage(message)
89 | );
90 |
91 | if (hasErrors) {
92 | console.log(chalk.red('Failed to compile.'));
93 | console.log();
94 | if (formattedErrors.some(isLikelyASyntaxError)) {
95 | // If there are any syntax errors, show just them.
96 | // This prevents a confusing ESLint parsing error
97 | // preceding a much more useful Babel syntax error.
98 | formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
99 | }
100 | formattedErrors.forEach(message => {
101 | console.log(message);
102 | console.log();
103 | });
104 | // If errors exist, ignore warnings.
105 | return;
106 | }
107 |
108 | if (hasWarnings) {
109 | console.log(chalk.yellow('Compiled with warnings.'));
110 | console.log();
111 | formattedWarnings.forEach(message => {
112 | console.log(message);
113 | console.log();
114 | });
115 |
116 | console.log('You may use special comments to disable some warnings.');
117 | console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.');
118 | console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
119 | }
120 | });
121 | }
122 |
123 | function openBrowser(port) {
124 | if (process.platform === 'darwin') {
125 | try {
126 | // Try our best to reuse existing tab
127 | // on OS X Google Chrome with AppleScript
128 | execSync('ps cax | grep "Google Chrome"');
129 | execSync(
130 | 'osascript ' +
131 | path.resolve(__dirname, './utils/chrome.applescript') +
132 | ' http://localhost:' + port + '/'
133 | );
134 | return;
135 | } catch (err) {
136 | // Ignore errors.
137 | }
138 | }
139 | // Fallback to opn
140 | // (It will always open new tab)
141 | opn('http://localhost:' + port + '/');
142 | }
143 |
144 | function runDevServer(port) {
145 | new WebpackDevServer(compiler, {
146 | historyApiFallback: true,
147 | hot: true, // Note: only CSS is currently hot reloaded
148 | publicPath: config.output.publicPath,
149 | quiet: true,
150 | watchOptions: {
151 | ignored: /node_modules/
152 | }
153 | }).listen(port, (err, result) => {
154 | if (err) {
155 | return console.log(err);
156 | }
157 |
158 | clearConsole();
159 | console.log(chalk.cyan('Starting the development server...'));
160 | console.log();
161 | openBrowser(port);
162 | });
163 | }
164 |
165 | function run(port) {
166 | setupCompiler(port);
167 | runDevServer(port);
168 | }
169 |
170 | detect(DEFAULT_PORT).then(port => {
171 | if (port === DEFAULT_PORT) {
172 | run(port);
173 | return;
174 | }
175 |
176 | clearConsole();
177 | var question =
178 | chalk.yellow('Something is already running at port ' + DEFAULT_PORT + '.') +
179 | '\n\nWould you like to run the app at another port instead?';
180 |
181 | prompt(question, true).then(shouldChangePort => {
182 | if (shouldChangePort) {
183 | run(port);
184 | }
185 | });
186 | });
187 |
--------------------------------------------------------------------------------
/scripts/utils/chrome.applescript:
--------------------------------------------------------------------------------
1 | on run argv
2 | set theURL to item 1 of argv
3 |
4 | tell application "Chrome"
5 |
6 | if (count every window) = 0 then
7 | make new window
8 | end if
9 |
10 | -- Find a tab currently running the debugger
11 | set found to false
12 | set theTabIndex to -1
13 | repeat with theWindow in every window
14 | set theTabIndex to 0
15 | repeat with theTab in every tab of theWindow
16 | set theTabIndex to theTabIndex + 1
17 | if theTab's URL is theURL then
18 | set found to true
19 | exit repeat
20 | end if
21 | end repeat
22 |
23 | if found then
24 | exit repeat
25 | end if
26 | end repeat
27 |
28 | if found then
29 | tell theTab to reload
30 | set index of theWindow to 1
31 | set theWindow's active tab index to theTabIndex
32 | else
33 | tell window 1
34 | activate
35 | make new tab with properties {URL:theURL}
36 | end tell
37 | end if
38 | end tell
39 | end run
40 |
--------------------------------------------------------------------------------
/scripts/utils/detectPort.js:
--------------------------------------------------------------------------------
1 | /* ================================================================
2 | * detect-port by xdf(xudafeng[at]126.com)
3 | *
4 | * first created at : Tue Mar 17 2015 00:16:10 GMT+0800 (CST)
5 | *
6 | * ================================================================
7 | * Copyright xdf
8 | *
9 | * Licensed under the MIT License
10 | * You may not use this file except in compliance with the License.
11 | *
12 | * ================================================================ */
13 |
14 | // We are forking this temporarily to resolve
15 | // https://github.com/facebookincubator/create-react-app/issues/302.
16 |
17 | // We can replace this fork with `detect-port` package when this is merged:
18 | // https://github.com/xudafeng/detect-port/pull/4.
19 |
20 | 'use strict';
21 |
22 | var net = require('net');
23 |
24 | var inject = function(port) {
25 | var options = global.__detect ? global.__detect.options : {};
26 |
27 | if (options.verbose) {
28 | console.log('port %d was occupied', port);
29 | }
30 | };
31 |
32 | function detect(port, fn) {
33 |
34 | var _detect = function(port) {
35 | return new Promise(function(resolve, reject) {
36 | var socket = new net.Socket();
37 | socket.once('error', function() {
38 | socket.removeAllListeners('connect');
39 | socket.removeAllListeners('error');
40 | socket.end();
41 | socket.destroy();
42 | socket.unref();
43 | var server = new net.Server();
44 | server.on('error', function() {
45 | inject(port);
46 | port++;
47 | resolve(_detect(port));
48 | });
49 |
50 | server.listen(port, function() {
51 | server.once('close', function() {
52 | resolve(port);
53 | });
54 | server.close();
55 | });
56 | });
57 | socket.once('connect', function() {
58 | inject(port);
59 | port++;
60 | resolve(_detect(port));
61 | socket.removeAllListeners('connect');
62 | socket.removeAllListeners('error');
63 | socket.end();
64 | socket.destroy();
65 | socket.unref();
66 | });
67 | socket.connect({
68 | port: port
69 | });
70 | });
71 | }
72 |
73 | var _detect_with_cb = function(_fn) {
74 | _detect(port)
75 | .then(function(result) {
76 | _fn(null, result);
77 | })
78 | .catch(function(e) {
79 | _fn(e);
80 | });
81 | };
82 |
83 | return fn ? _detect_with_cb(fn) : _detect(port);
84 | }
85 |
86 | module.exports = detect;
87 |
--------------------------------------------------------------------------------
/scripts/utils/prompt.js:
--------------------------------------------------------------------------------
1 | var rl = require('readline');
2 |
3 | // Convention: "no" should be the conservative choice.
4 | // If you mistype the answer, we'll always take it as a "no".
5 | // You can control the behavior on with `isYesDefault`.
6 | module.exports = function (question, isYesDefault) {
7 | if (typeof isYesDefault !== 'boolean') {
8 | throw new Error('Provide explicit boolean isYesDefault as second argument.');
9 | }
10 | return new Promise(resolve => {
11 | var rlInterface = rl.createInterface({
12 | input: process.stdin,
13 | output: process.stdout,
14 | });
15 |
16 | var hint = isYesDefault === true ? '[Y/n]' : '[y/N]';
17 | var message = question + ' ' + hint + '\n';
18 |
19 | rlInterface.question(message, function(answer) {
20 | rlInterface.close();
21 |
22 | var useDefault = answer.trim().length === 0;
23 | if (useDefault) {
24 | return resolve(isYesDefault);
25 | }
26 |
27 | var isYes = answer.match(/^(yes|y)$/i);
28 | return resolve(isYes);
29 | });
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-intro {
18 | font-size: large;
19 | }
20 |
21 | @keyframes App-logo-spin {
22 | from { transform: rotate(0deg); }
23 | to { transform: rotate(360deg); }
24 | }
25 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import logo from './logo.svg';
3 | import './App.css';
4 | import PeopleContainer from './components/PeopleContainer';
5 |
6 | class App extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |

12 |
Welcome to React
13 |
14 |
15 | To get started, edit src/App.js
and save to reload.
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default App;
24 |
--------------------------------------------------------------------------------
/src/actions/action-types.js:
--------------------------------------------------------------------------------
1 | export const ADD_PERSON = 'ADD_PERSON';
2 |
--------------------------------------------------------------------------------
/src/actions/people-actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './action-types';
2 |
3 | export const addPerson = (person) => {
4 | return {
5 | type: types.ADD_PERSON,
6 | person
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/PeopleContainer.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {connect} from 'react-redux';
3 | import {bindActionCreators} from 'redux';
4 | import * as peopleActions from '../actions/people-actions';
5 | import PeopleList from './PeopleList';
6 | import PersonInput from './PersonInput';
7 |
8 | class PeopleContainer extends Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | people: []
14 | };
15 | }
16 |
17 | render() {
18 | const {people} = this.props;
19 |
20 | return (
21 |
25 | );
26 | }
27 | }
28 |
29 | PeopleContainer.propTypes = {
30 | people: PropTypes.array.isRequired,
31 | actions: PropTypes.object.isRequired
32 | };
33 |
34 | function mapStateToProps(state, props) {
35 | return {
36 | people: state.people
37 | };
38 | }
39 |
40 | function mapDispatchToProps(dispatch) {
41 | return {
42 | actions: bindActionCreators(peopleActions, dispatch)
43 | }
44 | }
45 |
46 | export default connect(mapStateToProps, mapDispatchToProps)(PeopleContainer);
47 |
--------------------------------------------------------------------------------
/src/components/PeopleList.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 | import Person from './Person';
3 |
4 | const PeopleList = ({people}) => {
5 | return (
6 |
7 | {people.map((person) =>
8 |
9 | )}
10 |
11 | );
12 | };
13 |
14 | PeopleList.propTypes = {
15 | people: PropTypes.array.isRequired
16 | };
17 |
18 | export default PeopleList;
19 |
--------------------------------------------------------------------------------
/src/components/Person.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 |
3 | const Person = ({person}) => {
4 | return (
5 |
6 | {person.lastname}, {person.firstname}
7 |
8 | );
9 | };
10 |
11 | Person.propTypes = {
12 | person: PropTypes.object.isRequired
13 | };
14 |
15 | export default Person;
16 |
--------------------------------------------------------------------------------
/src/components/PersonInput.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | class PersonInput extends Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.onAddPersonClick = this.onAddPersonClick.bind(this);
8 | }
9 |
10 | onAddPersonClick() {
11 | const firstNameElement = document.getElementById('firstname');
12 | const lastNameElement = document.getElementById('lastname');
13 |
14 | this.props.addPerson({
15 | firstname: firstNameElement.value,
16 | lastname: lastNameElement.value
17 | });
18 |
19 | firstNameElement.value = "";
20 | lastNameElement.value = "";
21 |
22 | firstNameElement.focus();
23 | }
24 |
25 | componentDidMount() {
26 | document.getElementById('firstname').focus();
27 | }
28 |
29 | render() {
30 | return (
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | PersonInput.propTypes = {
41 | addPerson: PropTypes.func.isRequired
42 | };
43 |
44 | export default PersonInput;
45 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 | import {Provider} from 'react-redux';
6 | import configureStore from './store/configure-store';
7 |
8 | const store = configureStore();
9 |
10 | ReactDOM.render(
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import people from './people-reducer.js';
2 | import {combineReducers} from 'redux';
3 |
4 | const rootReducer = combineReducers({
5 | people
6 | });
7 |
8 | export default rootReducer;
9 |
--------------------------------------------------------------------------------
/src/reducers/people-reducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/action-types';
2 |
3 | export default (state = [], action) => {
4 | switch (action.type) {
5 | case types.ADD_PERSON:
6 | return [...state, Object.assign({}, action.person)];
7 | default:
8 | return state;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/store/configure-store.js:
--------------------------------------------------------------------------------
1 | import rootReducer from '../reducers';
2 | import {createStore, compose} from 'redux';
3 |
4 | // enable redux devtools... can this be done with Webpack instead?
5 | const enhancers = compose(
6 | window.devToolsExtension ? window.devToolsExtension() : f => f
7 | )
8 |
9 | export default (initialState) => {
10 | return createStore(rootReducer, initialState, enhancers);
11 | };
12 |
--------------------------------------------------------------------------------