├── .babelrc
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
├── webpack.config.common.js
├── webpack.config.dev.js
└── webpack.config.prod.js
├── package-lock.json
├── package.json
└── src
├── client
├── App.js
├── api
│ ├── actions.js
│ ├── index.js
│ └── reducer.js
├── components
│ ├── AboutWindow.js
│ ├── ConfirmationDialog.js
│ ├── ErrorWindow.js
│ ├── FlatButton.js
│ ├── Header
│ │ ├── Header.js
│ │ └── Header.scss
│ ├── NavigationButton.js
│ ├── ProgressBar.js
│ └── Window.js
├── containers
│ ├── AppBar.js
│ ├── AppLayout.js
│ ├── AppMenu.js
│ ├── LoginForm.js
│ ├── ModalsLayout
│ │ ├── ModalsLayout.js
│ │ ├── actions.js
│ │ └── reducer.js
│ ├── NotFound.js
│ └── ServersPage
│ │ ├── ServersPage.js
│ │ └── ServersPage.scss
├── index.html
├── index.js
├── logger.js
├── muiTheme.js
├── reducers.js
├── router.js
├── store.js
└── style.scss
└── server
├── index.js
├── logger.js
└── middlewares
├── api.js
├── development.js
└── production.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "retainLines": true,
3 | "plugins": ["react-hot-loader/babel", "@babel/plugin-proposal-class-properties"],
4 | "presets": [
5 | [
6 | "@babel/preset-env",
7 | {
8 | "modules": false,
9 | "targets": {
10 | "browsers": ["> 0.5%", "not dead"]
11 | }
12 | }
13 | ],
14 | "@babel/preset-react"
15 | ],
16 | "env": {
17 | "test": {
18 | "presets": ["@babel/preset-env", "@babel/preset-react"]
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true,
6 | "node": true
7 | },
8 | "extends": [
9 | "fbjs",
10 | "plugin:prettier/recommended"
11 | ],
12 | "parser": "babel-eslint",
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "plugins": [
17 | "prettier",
18 | "jsx-a11y",
19 | "react"
20 | ],
21 | "settings": {
22 | "import/resolver": {
23 | "webpack": {
24 | "config": "./config/webpack.config.prod.js"
25 | }
26 | },
27 | "react": {
28 | "version": "16.4.1"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | package-lock.json -diff
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.code-workspace
2 | *.log
3 |
4 | .tags*
5 | .history
6 |
7 | build
8 | build-dev
9 | node_modules
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "none",
8 | "bracketSpacing": false,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "always"
11 | }
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | git:
2 | depth: 3
3 | branches:
4 | except:
5 | - docs
6 | language: node_js
7 | node_js:
8 | - "node"
9 | - "8.9"
10 | script:
11 | - npm run lint
12 | - npm run build
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | # [2.4.0](https://github.com/antonfisher/react-express-webpack/compare/v2.3.0...v2.4.0) (2018-08-13)
7 |
8 |
9 | ### Bug Fixes
10 |
11 | * add LICENSE file ([4e9886f](https://github.com/antonfisher/react-express-webpack/commit/4e9886f))
12 | * add plugin for the version of postcss ([431a293](https://github.com/antonfisher/react-express-webpack/commit/431a293))
13 | * disable 'add server' button while loading ([d277845](https://github.com/antonfisher/react-express-webpack/commit/d277845))
14 | * set default fetch mode to cors ([142dc78](https://github.com/antonfisher/react-express-webpack/commit/142dc78))
15 | * show Log Out item on the last position in menu ([507c4c6](https://github.com/antonfisher/react-express-webpack/commit/507c4c6))
16 |
17 |
18 | ### Features
19 |
20 | * update winston logger to v3 ([8005871](https://github.com/antonfisher/react-express-webpack/commit/8005871))
21 | * use components names as file names instead of index.js ([ac0465f](https://github.com/antonfisher/react-express-webpack/commit/ac0465f))
22 |
23 |
24 |
25 |
26 | # [2.3.0](https://github.com/antonfisher/react-express-webpack/compare/v2.2.0...v2.3.0) (2018-05-10)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * move dev deps to dev deps ([e7a42bc](https://github.com/antonfisher/react-express-webpack/commit/e7a42bc))
32 |
33 |
34 | ### Features
35 |
36 | * update deps, express@4.16.3, webpack@4.8.1, react@16.3.2 ([b5aa7fa](https://github.com/antonfisher/react-express-webpack/commit/b5aa7fa))
37 | * upgrade to prettier@1.12.1 ([299ddc3](https://github.com/antonfisher/react-express-webpack/commit/299ddc3))
38 |
39 |
40 |
41 |
42 | # [2.2.0](https://github.com/antonfisher/react-express-webpack/compare/v2.1.0...v2.2.0) (2018-03-12)
43 |
44 |
45 | ### Features
46 |
47 | * hide package-lock.json from git diff output ([6b63ff2](https://github.com/antonfisher/react-express-webpack/commit/6b63ff2))
48 | * upgrade to Webpack 4 ([f6224bc](https://github.com/antonfisher/react-express-webpack/commit/f6224bc))
49 | * use compact build output ([5d5bb55](https://github.com/antonfisher/react-express-webpack/commit/5d5bb55))
50 | * use last version of uglifyjs ([cd71f35](https://github.com/antonfisher/react-express-webpack/commit/cd71f35))
51 | * use react-hot-loader v4 ([121bda7](https://github.com/antonfisher/react-express-webpack/commit/121bda7))
52 | * use webpack merge for merging configs ([d765ec6](https://github.com/antonfisher/react-express-webpack/commit/d765ec6))
53 |
54 |
55 |
56 |
57 | # [2.1.0](https://github.com/antonfisher/react-express-webpack/compare/v2.0.0...v2.1.0) (2018-02-01)
58 |
59 |
60 | ### Bug Fixes
61 |
62 | * always include Intl to main bundle ([7b9a36f](https://github.com/antonfisher/react-express-webpack/commit/7b9a36f))
63 |
64 |
65 | ### Features
66 |
67 | * update packadge.json for windows support (closes [#3](https://github.com/antonfisher/react-express-webpack/issues/3)) ([2b8f7ac](https://github.com/antonfisher/react-express-webpack/commit/2b8f7ac))
68 |
69 |
70 |
71 |
72 | # [2.0.0](https://github.com/antonfisher/react-express-webpack/compare/v1.2.0...v2.0.0) (2018-01-27)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * move html-webpack-harddisk-plugin dependency to dev ([9fe7747](https://github.com/antonfisher/react-express-webpack/commit/9fe7747))
78 | * remove unused dependency 'history' ([75bc02f](https://github.com/antonfisher/react-express-webpack/commit/75bc02f))
79 | * remove unused gif file-loader rule ([f121d0b](https://github.com/antonfisher/react-express-webpack/commit/f121d0b))
80 | * use application logger in webpack-dev-middleware ([badfbc0](https://github.com/antonfisher/react-express-webpack/commit/badfbc0))
81 |
82 |
83 | ### Features
84 |
85 | * remove react-tap-event-plugin dependency ([a6cd1e9](https://github.com/antonfisher/react-express-webpack/commit/a6cd1e9))
86 | * support for realy old browsers, add intl polyfill (closes [#2](https://github.com/antonfisher/react-express-webpack/issues/2)) ([94f4e95](https://github.com/antonfisher/react-express-webpack/commit/94f4e95))
87 | * update React to 16.2.0 ([4f9bd1b](https://github.com/antonfisher/react-express-webpack/commit/4f9bd1b))
88 | * upgrade to react 16 ([1f73233](https://github.com/antonfisher/react-express-webpack/commit/1f73233))
89 | * use flat componets/containers structure ([e44986b](https://github.com/antonfisher/react-express-webpack/commit/e44986b))
90 | * use prettier code formatter ([894f014](https://github.com/antonfisher/react-express-webpack/commit/894f014))
91 |
92 |
93 |
94 |
95 | # [1.2.0](https://github.com/antonfisher/react-express-webpack/compare/v1.1.0...v1.2.0) (2017-09-22)
96 |
97 |
98 | ### Bug Fixes
99 |
100 | * don't sync router with redux store with old way ([70b0d1d](https://github.com/antonfisher/react-express-webpack/commit/70b0d1d))
101 |
102 |
103 | ### Features
104 |
105 | * use '/' as root page url ([30b75b6](https://github.com/antonfisher/react-express-webpack/commit/30b75b6))
106 |
107 |
108 |
109 |
110 | # [1.1.0](https://github.com/antonfisher/react-express-webpack/compare/v1.0.3...v1.1.0) (2017-09-22)
111 |
112 |
113 | ### Features
114 |
115 | * add ip to logger output, don't log query starting ([88c589f](https://github.com/antonfisher/react-express-webpack/commit/88c589f))
116 | * add unhandled errors handling, rename APP_PORT to HTTP_PORT, log app env ([8584748](https://github.com/antonfisher/react-express-webpack/commit/8584748))
117 | * remove separated app config file ([ce0d66f](https://github.com/antonfisher/react-express-webpack/commit/ce0d66f))
118 |
119 |
120 |
121 |
122 | ## [1.0.2](https://github.com/antonfisher/react-express-webpack/compare/v1.0.0...v1.0.2) (2017-04-28)
123 |
124 |
125 | ### Bug Fixes
126 |
127 | * always show response status code on error ([fcaf20b](https://github.com/antonfisher/react-express-webpack/commit/fcaf20b))
128 | * modals don't work properly in production mode ([3e07ec4](https://github.com/antonfisher/react-express-webpack/commit/3e07ec4))
129 | * remove unused code from venders file ([922ac69](https://github.com/antonfisher/react-express-webpack/commit/922ac69))
130 | * set valid entries order for react-hot-loader ([5a0191e](https://github.com/antonfisher/react-express-webpack/commit/5a0191e))
131 | * use both end and close events in logger ([123f5c0](https://github.com/antonfisher/react-express-webpack/commit/123f5c0))
132 |
133 |
134 | ### Features
135 |
136 | * add url, explanation and status code to api error messages ([95a1613](https://github.com/antonfisher/react-express-webpack/commit/95a1613))
137 | * migrate to react-router v4 ([3dfecd7](https://github.com/antonfisher/react-express-webpack/commit/3dfecd7))
138 | * use winston as default logger ([a554ec3](https://github.com/antonfisher/react-express-webpack/commit/a554ec3))
139 |
140 |
141 |
142 |
143 | ## [1.0.1](https://github.com/antonfisher/react-express-webpack/compare/v1.0.0...v1.0.1) (2017-04-28)
144 |
145 |
146 | ### Bug Fixes
147 |
148 | * always show response status code on error ([fcaf20b](https://github.com/antonfisher/react-express-webpack/commit/fcaf20b))
149 | * modals don't work properly in production mode ([3e07ec4](https://github.com/antonfisher/react-express-webpack/commit/3e07ec4))
150 | * remove unused code from venders file ([922ac69](https://github.com/antonfisher/react-express-webpack/commit/922ac69))
151 | * set valid entries order for react-hot-loader ([5a0191e](https://github.com/antonfisher/react-express-webpack/commit/5a0191e))
152 | * use both end and close events in logger ([123f5c0](https://github.com/antonfisher/react-express-webpack/commit/123f5c0))
153 |
154 |
155 | ### Features
156 |
157 | * add url, explanation and status code to api error messages ([95a1613](https://github.com/antonfisher/react-express-webpack/commit/95a1613))
158 | * migrate to react-router v4 ([3dfecd7](https://github.com/antonfisher/react-express-webpack/commit/3dfecd7))
159 | * use winston as default logger ([a554ec3](https://github.com/antonfisher/react-express-webpack/commit/a554ec3))
160 |
161 |
162 |
163 |
164 | # 1.0.0 (2017-04-19)
165 |
166 |
167 | ### Bug Fixes
168 |
169 | * add npm build to travis ([f7b353b](https://github.com/antonfisher/react-express-webpack/commit/f7b353b))
170 | * lint errors ([17a5f7e](https://github.com/antonfisher/react-express-webpack/commit/17a5f7e))
171 | * lints errors after upgrade ([cbb0eaa](https://github.com/antonfisher/react-express-webpack/commit/cbb0eaa))
172 |
173 |
174 | ### Features
175 |
176 | * add travis ci and badges ([db7bc3d](https://github.com/antonfisher/react-express-webpack/commit/db7bc3d))
177 | * migrate to React 15.5.4 ([dac4421](https://github.com/antonfisher/react-express-webpack/commit/dac4421))
178 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Anton Fisher
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 | React boilerplate with ES2015, Express.js, and Webpack
2 |
3 | [](https://travis-ci.org/antonfisher/react-express-webpack)
4 | [](https://github.com/antonfisher/react-express-webpack/blob/master/LICENSE)
5 |
6 | ## Technologies
7 |
8 | - React (v16) + Redux (v3) + React Router (v4)
9 | - Express.js (v4) as production and development server
10 | - Webpack 4 (production and development configurations)
11 | - SCSS support (+ sanitize.css included)
12 | - ES2015+
13 |
14 | ## Features
15 | - preconfigured router
16 | - React Material UI example theme
17 | - preconfigured modal windows
18 | - preconfigured eslint and Prettier code formatter
19 | - React Hot Loader
20 | - Linux/MacOS/Windows
21 |
22 | ## Screenshots
23 |
24 | Demo UI view:
25 |
26 | 
27 |
28 | Development `.js` bundles:
29 |
30 | 
31 |
32 | Production `.js` bundles:
33 |
34 | 
35 |
36 | Production mode server output:
37 |
38 | 
39 |
40 | ## Usage
41 |
42 | ### Installation
43 | ```bash
44 | git clone git@github.com:antonfisher/react-express-webpack.git
45 | cd react-express-webpack
46 | npm install
47 |
48 | # remove boilerplate git references
49 | rm ./.git
50 | ```
51 |
52 | ### Scripts
53 | ```bash
54 | # run development mode
55 | npm run dev
56 |
57 | # run production mode
58 | npm run build
59 | npm start
60 |
61 | # run prettier
62 | npm run prettier
63 |
64 | # run lint
65 | npm run lint
66 |
67 | # run on a different port
68 | HTTP_PORT=3001 npm run dev
69 | ```
70 |
71 | ## License
72 | MIT License. Free use and change.
73 |
--------------------------------------------------------------------------------
/config/webpack.config.common.js:
--------------------------------------------------------------------------------
1 | const {resolve, join} = require('path');
2 | const webpack = require('webpack');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | const postcssPresetEnv = require('postcss-preset-env');
5 |
6 | const IS_DEV = process.env.NODE_ENV !== 'production';
7 |
8 | module.exports = {
9 | target: 'web',
10 | entry: ['./src/client/index.js'],
11 | output: {
12 | publicPath: '/',
13 | path: resolve(__dirname, '..', 'build', 'client'),
14 | filename: '[name].js'
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.js$/,
20 | use: ['babel-loader'],
21 | exclude: /node_modules/
22 | },
23 | {
24 | test: /\.html$/,
25 | loader: 'html-loader'
26 | },
27 | {
28 | test: /\.s?css$/,
29 | use: ExtractTextPlugin.extract({
30 | fallback: {
31 | loader: 'style-loader',
32 | options: {sourceMap: IS_DEV}
33 | },
34 | use: [
35 | {
36 | loader: 'css-loader',
37 | options: {
38 | localIdentName: IS_DEV ? '[path]-[name]_[local]' : '[name]_[local]_[hash:5]', // [hash:base64]
39 | modules: true,
40 | sourceMap: IS_DEV
41 | }
42 | },
43 | {
44 | loader: 'sass-loader',
45 | options: {sourceMap: IS_DEV}
46 | },
47 | {
48 | loader: 'postcss-loader',
49 | options: {
50 | ident: 'postcss',
51 | plugins: () => [postcssPresetEnv()],
52 | sourceMap: IS_DEV
53 | }
54 | }
55 | ]
56 | })
57 | },
58 | {
59 | test: /\.(eot|svg|ttf|woff|woff2)$/,
60 | loader: 'file-loader'
61 | }
62 | ]
63 | },
64 | plugins: [
65 | new ExtractTextPlugin({
66 | filename: '[name].css',
67 | disable: IS_DEV
68 | }),
69 | new webpack.EnvironmentPlugin(['NODE_ENV'])
70 | ],
71 | resolve: {
72 | modules: ['node_modules', join('src', 'client')]
73 | },
74 | optimization: {
75 | splitChunks: {
76 | cacheGroups: {
77 | commons: {
78 | test: /[\\/]node_modules[\\/]/,
79 | name: 'vendor',
80 | chunks: 'all'
81 | }
82 | }
83 | }
84 | },
85 | stats: {
86 | assetsSort: '!size',
87 | children: false,
88 | chunks: false,
89 | colors: true,
90 | entrypoints: false,
91 | modules: false
92 | }
93 | };
94 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path');
2 | const webpack = require('webpack');
3 | const merge = require('webpack-merge');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
6 |
7 | const commonConfig = require('./webpack.config.common');
8 |
9 | module.exports = merge(commonConfig, {
10 | mode: 'development',
11 | entry: [`webpack-hot-middleware/client?http://localhost:${process.env.HTTP_PORT}&reload=true`],
12 | output: {
13 | hotUpdateMainFilename: 'hot-update.[hash:6].json',
14 | hotUpdateChunkFilename: 'hot-update.[hash:6].js'
15 | },
16 | devtool: 'cheap-module-eval-source-map',
17 | plugins: [
18 | new webpack.HotModuleReplacementPlugin(),
19 | new webpack.NamedModulesPlugin(),
20 | new HtmlWebpackPlugin({
21 | inject: true,
22 | template: resolve(__dirname, '..', 'src', 'client', 'index.html'),
23 | //favicon: resolve(__dirname, '..', 'src', 'client', 'static', 'favicon.png'),
24 | alwaysWriteToDisk: true
25 | }),
26 | new HtmlWebpackHarddiskPlugin({
27 | outputPath: resolve(__dirname, '..', 'build-dev', 'client')
28 | })
29 | ]
30 | });
31 |
--------------------------------------------------------------------------------
/config/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path');
2 | const merge = require('webpack-merge');
3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | const commonConfig = require('./webpack.config.common');
7 |
8 | module.exports = merge(commonConfig, {
9 | mode: 'production',
10 | plugins: [
11 | new UglifyJsPlugin({
12 | parallel: true,
13 | extractComments: true
14 | }),
15 | new HtmlWebpackPlugin({
16 | hash: true,
17 | inject: true,
18 | template: resolve(__dirname, '..', 'src', 'client', 'index.html'),
19 | //favicon: resolve(__dirname, '..', 'src', 'client', 'static', 'favicon.png'),
20 | minify: {
21 | removeComments: true,
22 | collapseWhitespace: true,
23 | removeRedundantAttributes: true,
24 | useShortDoctype: true,
25 | removeEmptyAttributes: true,
26 | removeStyleLinkTypeAttributes: true,
27 | keepClosingSlash: true,
28 | minifyJS: true,
29 | minifyCSS: true,
30 | minifyURLs: true
31 | }
32 | })
33 | ]
34 | });
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.4.0",
3 | "author": {
4 | "name": "Anton Fisher",
5 | "email": "a.fschr@gmail.com",
6 | "url": "http://antonfisher.com"
7 | },
8 | "description": "React boilerplate with ES2015, Express.js, and Webpack",
9 | "license": "MIT",
10 | "engines": {
11 | "node": "8.11.1"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/antonfisher/react-express-webpack.git"
16 | },
17 | "scripts": {
18 | "dev": "cross-env NODE_ENV=development node src/server",
19 | "start": "cross-env NODE_ENV=production node build/server",
20 | "prebuild": "rimraf build",
21 | "build": "npm run build:client && npm run build:server",
22 | "build:client": "cross-env NODE_ENV=production webpack -p --progress --config=config/webpack.config.prod.js",
23 | "build:server": "copyfiles -a -u 1 src/server/**/**/* build",
24 | "lint": "eslint --ignore-path .gitignore --ignore-pattern node_modules -- .",
25 | "prettier": "prettier --ignore-path .gitignore --write './**/*.js'",
26 | "test": "exit 1",
27 | "release:patch": "npm run lint && npx standard-version -r patch && git push --follow-tags origin master",
28 | "release:minor": "npm run lint && npx standard-version -r minor && git push --follow-tags origin master",
29 | "release:major": "npm run lint && npx standard-version -r major && git push --follow-tags origin master"
30 | },
31 | "postcss": {},
32 | "devDependencies": {
33 | "@babel/cli": "7.2.3",
34 | "@babel/core": "7.2.2",
35 | "@babel/plugin-proposal-class-properties": "7.3.0",
36 | "@babel/polyfill": "7.2.5",
37 | "@babel/preset-env": "7.3.1",
38 | "@babel/preset-react": "7.0.0",
39 | "babel-core": "7.0.0-bridge.0",
40 | "babel-eslint": "10.0.1",
41 | "babel-loader": "8.0.5",
42 | "babel-preset-stage-0": "6.24.1",
43 | "babel-watch": "2.0.8",
44 | "copyfiles": "2.1.0",
45 | "css-loader": "2.1.0",
46 | "eslint": "5.12.1",
47 | "eslint-config-fbjs": "2.1.0",
48 | "eslint-config-prettier": "4.0.0",
49 | "eslint-import-resolver-webpack": "0.11.0",
50 | "eslint-plugin-babel": "5.3.0",
51 | "eslint-plugin-flowtype": "3.2.1",
52 | "eslint-plugin-import": "2.15.0",
53 | "eslint-plugin-jsx-a11y": "6.2.0",
54 | "eslint-plugin-prettier": "3.0.1",
55 | "eslint-plugin-react": "7.12.4",
56 | "eslint-plugin-relay": "1.0.0",
57 | "extract-text-webpack-plugin": "4.0.0-beta.0",
58 | "html-loader": "0.5.5",
59 | "html-webpack-harddisk-plugin": "1.0.1",
60 | "html-webpack-plugin": "3.2.0",
61 | "immutable": "3.8.2",
62 | "intl": "1.2.5",
63 | "material-ui": "0.20.2",
64 | "node-sass": "4.11.0",
65 | "postcss-loader": "3.0.0",
66 | "postcss-preset-env": "6.5.0",
67 | "prettier": "1.16.1",
68 | "prop-types": "15.6.2",
69 | "react": "16.7.0",
70 | "react-dom": "16.7.0",
71 | "react-hot-loader": "4.6.3",
72 | "react-intl": "2.8.0",
73 | "react-redux": "6.0.0",
74 | "react-router-dom": "4.3.1",
75 | "react-router-redux": "4.0.8",
76 | "redux": "4.0.1",
77 | "redux-actions": "2.6.4",
78 | "redux-logger": "3.0.6",
79 | "redux-thunk": "2.3.0",
80 | "rimraf": "2.6.3",
81 | "sanitize.css": "8.0.0",
82 | "sass-loader": "7.1.0",
83 | "style-loader": "0.23.1",
84 | "uglifyjs-webpack-plugin": "2.1.1",
85 | "webpack": "4.29.0",
86 | "webpack-cli": "3.2.1",
87 | "webpack-dev-middleware": "3.5.1",
88 | "webpack-hot-middleware": "2.24.3",
89 | "webpack-merge": "4.2.1",
90 | "whatwg-fetch": "3.0.0"
91 | },
92 | "dependencies": {
93 | "body-parser": "1.18.3",
94 | "compression": "1.7.3",
95 | "cross-env": "5.2.0",
96 | "express": "4.16.4",
97 | "fast-safe-stringify": "2.0.6",
98 | "winston": "3.2.0"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/client/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {hot} from 'react-hot-loader';
3 | import {Provider} from 'react-redux';
4 | import {IntlProvider} from 'react-intl';
5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
6 |
7 | import store from './store';
8 | import Router from './router';
9 | import muiTheme from './muiTheme';
10 |
11 | class App extends React.Component {
12 | render() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | export default hot(module)(App);
26 |
--------------------------------------------------------------------------------
/src/client/api/actions.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'redux-actions';
2 |
3 | import api from 'api/index';
4 | import {showModal} from 'containers/ModalsLayout/actions';
5 | import ErrorWindow from 'components/ErrorWindow';
6 |
7 | function* createGuidGenerator() {
8 | let i = 1;
9 | while (i) {
10 | yield i++;
11 | }
12 | }
13 |
14 | const guidGenerator = createGuidGenerator();
15 |
16 | export const API_REQUEST_STARTED = 'API_REQUEST_STARTED';
17 | export const apiRequestStarted = createAction(API_REQUEST_STARTED);
18 | export const API_REQUEST_FINISHED = 'API_REQUEST_FINISHED';
19 | export const apiRequestFinished = createAction(API_REQUEST_FINISHED);
20 |
21 | export const API_DATA_SERVERS_LOADED = 'API_DATA_SERVERS_LOADED';
22 | export const apiDataServersLoaded = createAction(API_DATA_SERVERS_LOADED);
23 |
24 | export function apiGetServers(callback) {
25 | return function dispatchApiGetServers(dispatch) {
26 | const requestId = guidGenerator.next().value;
27 | dispatch(apiRequestStarted({requestId}));
28 | return api
29 | .getStats()
30 | .then((data) => {
31 | dispatch(apiDataServersLoaded(data));
32 | dispatch(apiRequestFinished({requestId}));
33 | if (callback) {
34 | callback(); // get rid of callback here?
35 | }
36 | })
37 | .catch((error) => {
38 | dispatch(apiRequestFinished({requestId, error}));
39 | dispatch(
40 | showModal({
41 | key: ErrorWindow.NAME,
42 | props: {
43 | title: error.title,
44 | message: error.message,
45 | explanation: `URL: ${error.url} ${error.statusCode}`
46 | }
47 | })
48 | );
49 | });
50 | };
51 | }
52 |
53 | export function apiAddServer(data, callback) {
54 | return function dispatchApiAddServer(dispatch) {
55 | const requestId = guidGenerator.next().value;
56 | dispatch(apiRequestStarted({requestId}));
57 | return api
58 | .addServer(data)
59 | .then(() => {
60 | dispatch(apiGetServers());
61 | dispatch(apiRequestFinished({requestId}));
62 | if (callback) {
63 | callback(); // get rid of callback here?
64 | }
65 | })
66 | .catch((error) => {
67 | dispatch(apiRequestFinished({requestId, error}));
68 | dispatch(
69 | showModal({
70 | key: ErrorWindow.NAME,
71 | props: {
72 | title: error.title,
73 | message: error.message,
74 | explanation: `URL: ${error.url} ${error.statusCode}`
75 | }
76 | })
77 | );
78 | });
79 | };
80 | }
81 |
--------------------------------------------------------------------------------
/src/client/api/index.js:
--------------------------------------------------------------------------------
1 | const defaultParams = {
2 | mode: 'cors'
3 | };
4 |
5 | let endpoint = '';
6 |
7 | function setEndpoint(value) {
8 | endpoint = value;
9 | }
10 |
11 | /**
12 | * @param {String} url
13 | * @param {String} message
14 | * @param {Number} statusCode
15 | * @constructor
16 | */
17 | function ApiError(url, message, statusCode) {
18 | this.url = url;
19 | this.message = message;
20 | this.statusCode = statusCode || '';
21 | this.title = 'API Error';
22 | this.stack = new Error().stack;
23 | }
24 |
25 | /**
26 | * @param {String} url
27 | * @param {Error} error
28 | * @param {Number|undefined} statusCode
29 | * @throws {ApiError}
30 | */
31 | function throwApiError(url, error, statusCode) {
32 | throw new ApiError(url, error, statusCode);
33 | }
34 |
35 | /**
36 | * @param {Object} params
37 | * @returns {Promise.}
38 | * @private
39 | */
40 | function _request(params) {
41 | let requestUrl;
42 | let requestParams;
43 | if (typeof params === 'string') {
44 | requestUrl = params;
45 | requestParams = {};
46 | } else {
47 | const {url, ...restParams} = params;
48 | requestUrl = url;
49 | requestParams = restParams;
50 | }
51 |
52 | let rawResponse;
53 | return fetch(requestUrl, {...defaultParams, ...requestParams})
54 | .then((response) => {
55 | rawResponse = response;
56 | return response.json();
57 | })
58 | .then((json) => {
59 | if (json && json.error) {
60 | return throwApiError(requestUrl, json.error, rawResponse.status);
61 | }
62 | return json;
63 | })
64 | .catch((error) => throwApiError(requestUrl, error.message));
65 | }
66 |
67 | // application api
68 |
69 | function getStats() {
70 | return _request(`${endpoint}/stats`);
71 | }
72 |
73 | function addServer(payload) {
74 | return _request({
75 | url: `${endpoint}/servers`,
76 | method: 'POST',
77 | headers: new Headers({'content-type': 'application/json'}),
78 | body: JSON.stringify(payload),
79 | ...defaultParams
80 | });
81 | }
82 |
83 | export default {
84 | getStats,
85 | addServer,
86 | setEndpoint
87 | };
88 |
--------------------------------------------------------------------------------
/src/client/api/reducer.js:
--------------------------------------------------------------------------------
1 | import {List, Map, OrderedMap} from 'immutable';
2 |
3 | import {API_DATA_SERVERS_LOADED, API_REQUEST_FINISHED, API_REQUEST_STARTED} from 'api/actions';
4 |
5 | const initialState = Map({
6 | loading: false,
7 | requests: OrderedMap({}),
8 | errors: Map({
9 | last: null
10 | }),
11 | lastUpdate: Map({
12 | servers: null
13 | }),
14 | data: Map({
15 | servers: List()
16 | })
17 | });
18 |
19 | export default function ApiReducer(state = initialState, action) {
20 | switch (action.type) {
21 | case API_REQUEST_STARTED:
22 | return state.setIn(['requests', action.payload.requestId], action.payload).set('loading', true);
23 |
24 | case API_REQUEST_FINISHED:
25 | return state
26 | .removeIn(['requests', action.payload.requestId])
27 | .set('loading', state.get('requests').size > 1)
28 | .setIn(
29 | ['errors', 'last'],
30 | action.payload.error ? action.payload.error.message : state.getIn(['errors', 'last'])
31 | );
32 |
33 | case API_DATA_SERVERS_LOADED:
34 | return state
35 | .setIn(['lastUpdate', 'servers'], Date.now())
36 | .setIn(['data', 'servers'], List(action.payload.servers));
37 |
38 | default:
39 | return state;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/client/components/AboutWindow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Window from 'components/Window';
4 |
5 | class AboutWindow extends React.Component {
6 | static NAME = 'AboutWindow';
7 |
8 | static propTypes = {
9 | open: PropTypes.bool.isRequired,
10 | onHideModal: PropTypes.func.isRequired
11 | };
12 |
13 | render() {
14 | const {open, onHideModal} = this.props;
15 |
16 | return (
17 |
18 | Here is about window.
19 |
20 | );
21 | }
22 | }
23 |
24 | export default AboutWindow;
25 |
--------------------------------------------------------------------------------
/src/client/components/ConfirmationDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Dialog from 'material-ui/Dialog';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 |
6 | import FlatButton from 'components/FlatButton';
7 |
8 | class ConfirmationDialog extends React.Component {
9 | static NAME = 'ConfirmationDialog';
10 |
11 | static propTypes = {
12 | open: PropTypes.bool.isRequired,
13 | onHideModal: PropTypes.func.isRequired,
14 | onOk: PropTypes.func,
15 | onCancel: PropTypes.func,
16 | text: PropTypes.string
17 | };
18 |
19 | static defaultProps = {
20 | onOk: null,
21 | onCancel: null,
22 | text: 'Confirmation text?'
23 | };
24 |
25 | render() {
26 | const {open, text, onHideModal} = this.props;
27 | const onOk = () => (this.props.onOk ? this.props.onOk(onHideModal) : onHideModal());
28 | const onCancel = () => (this.props.onCancel ? this.props.onCancel(onHideModal) : onHideModal());
29 |
30 | const actions = [
31 | ,
32 |
33 | ];
34 |
35 | return (
36 |
39 | );
40 | }
41 | }
42 |
43 | export default ConfirmationDialog;
44 |
--------------------------------------------------------------------------------
/src/client/components/ErrorWindow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Window from 'components/Window';
5 |
6 | class ErrorWindow extends React.Component {
7 | static NAME = 'ErrorWindow';
8 |
9 | static propTypes = {
10 | open: PropTypes.bool.isRequired,
11 | onHideModal: PropTypes.func.isRequired,
12 | title: PropTypes.string,
13 | message: PropTypes.string,
14 | explanation: PropTypes.string
15 | };
16 |
17 | static defaultProps = {
18 | title: 'Error',
19 | message: 'Unnamed error occurred',
20 | explanation: ''
21 | };
22 |
23 | render() {
24 | const {open, title, message, explanation, onHideModal} = this.props;
25 |
26 | return (
27 |
28 | {message}
29 |
30 |
31 | {explanation}
32 |
33 | );
34 | }
35 | }
36 |
37 | export default ErrorWindow;
38 |
--------------------------------------------------------------------------------
/src/client/components/FlatButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import FlatButton from 'material-ui/FlatButton';
4 |
5 | class StyledFlatButton extends React.Component {
6 | static propTypes = {
7 | children: PropTypes.node,
8 | style: PropTypes.object
9 | };
10 |
11 | static defaultProps = {
12 | children: null,
13 | style: {}
14 | };
15 |
16 | render() {
17 | const style = Object.assign({}, {lineHeight: '33px'}, this.props.style);
18 |
19 | return (
20 |
21 | {React.Children.toArray(this.props.children)}
22 |
23 | );
24 | }
25 | }
26 |
27 | export default StyledFlatButton;
28 |
--------------------------------------------------------------------------------
/src/client/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import styles from './Header.scss';
5 |
6 | function Header({children}) {
7 | return {children}
;
8 | }
9 |
10 | Header.propTypes = {
11 | children: PropTypes.node.isRequired
12 | };
13 |
14 | export default Header;
15 |
--------------------------------------------------------------------------------
/src/client/components/Header/Header.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/src/client/components/NavigationButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {NavLink} from 'react-router-dom';
4 | import {lightGreen600} from 'material-ui/styles/colors';
5 |
6 | import FlatButton from 'components/FlatButton';
7 |
8 | class NavigationButton extends React.Component {
9 | static propTypes = {
10 | to: PropTypes.string.isRequired,
11 | label: PropTypes.string.isRequired,
12 | children: PropTypes.node,
13 | exact: PropTypes.bool
14 | };
15 |
16 | static defaultProps = {
17 | children: null,
18 | exact: false
19 | };
20 |
21 | render() {
22 | const {to, label, ...props} = this.props;
23 | const activeStyle = {backgroundColor: lightGreen600};
24 | const buttonStyle = {color: 'white', marginLeft: 0, marginRight: 1};
25 |
26 | return (
27 |
28 |
29 | {React.Children.toArray(this.props.children)}
30 |
31 |
32 | );
33 | }
34 | }
35 |
36 | export default NavigationButton;
37 |
--------------------------------------------------------------------------------
/src/client/components/ProgressBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import LinearProgress from 'material-ui/LinearProgress';
3 | import {teal500} from 'material-ui/styles/colors';
4 |
5 | export default class ProgressBar extends React.Component {
6 | render() {
7 | return ;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/components/Window.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Dialog from 'material-ui/Dialog';
4 | import RaisedButton from 'material-ui/RaisedButton';
5 |
6 | class Window extends React.Component {
7 | static propTypes = {
8 | open: PropTypes.bool.isRequired,
9 | onHideModal: PropTypes.func.isRequired,
10 | children: PropTypes.node
11 | };
12 |
13 | static defaultProps = {
14 | children: null
15 | };
16 |
17 | render() {
18 | const {children, open, onHideModal} = this.props;
19 |
20 | const props = {
21 | actions: [],
22 | ...this.props
23 | };
24 |
25 | return (
26 |
29 | );
30 | }
31 | }
32 |
33 | export default Window;
34 |
--------------------------------------------------------------------------------
/src/client/containers/AppBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import TextField from 'material-ui/TextField';
4 | import IconButton from 'material-ui/IconButton';
5 | import ActionSearchIcon from 'material-ui/svg-icons/action/search';
6 | import {Toolbar, ToolbarGroup} from 'material-ui/Toolbar';
7 | import AirplanemodeActiveIcon from 'material-ui/svg-icons/device/airplanemode-active';
8 |
9 | import NavigationButton from 'components/NavigationButton';
10 |
11 | export default class AppBar extends React.Component {
12 | static propTypes = {
13 | children: PropTypes.node.isRequired
14 | };
15 |
16 | render() {
17 | const {children} = this.props;
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 | {children}
38 |
39 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/client/containers/AppLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {Redirect, Route, Switch} from 'react-router-dom';
5 | import Paper from 'material-ui/Paper';
6 |
7 | import AppBar from 'containers/AppBar';
8 | import AppMenu from 'containers/AppMenu';
9 | import ModalsLayout from 'containers/ModalsLayout/ModalsLayout';
10 | import NotFound from 'containers/NotFound';
11 | import ServersPage from 'containers/ServersPage/ServersPage';
12 | import ProgressBar from 'components/ProgressBar';
13 |
14 | export class AppLayout extends React.Component {
15 | static propTypes = {
16 | loading: PropTypes.bool.isRequired
17 | };
18 |
19 | render() {
20 | const {loading} = this.props;
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | {loading && }
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | function mapStateToProps(state) {
44 | return {
45 | loading: state.api.get('loading')
46 | };
47 | }
48 |
49 | export default connect(mapStateToProps)(AppLayout);
50 |
--------------------------------------------------------------------------------
/src/client/containers/AppMenu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import IconMenu from 'material-ui/IconMenu';
5 | import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more';
6 | import ActionExitToAppIcon from 'material-ui/svg-icons/action/exit-to-app';
7 | import ActionHelpOutlineIcon from 'material-ui/svg-icons/action/help-outline';
8 | import ActionInfoOutlineIcon from 'material-ui/svg-icons/action/info-outline';
9 | import MenuItem from 'material-ui/MenuItem';
10 | import FlatButton from 'material-ui/FlatButton';
11 |
12 | import {showModal} from 'containers/ModalsLayout/actions';
13 | import {apiGetServers} from 'api/actions';
14 | import AboutWindow from 'components/AboutWindow';
15 | import ConfirmationDialog from 'components/ConfirmationDialog';
16 |
17 | export class AppMenu extends React.Component {
18 | static propTypes = {
19 | doLogout: PropTypes.func.isRequired,
20 | showAboutWindow: PropTypes.func.isRequired,
21 | showLogoutConfirmation: PropTypes.func.isRequired
22 | };
23 |
24 | constructor(...args) {
25 | super(...args);
26 | this.onShowAboutWindow = this.onShowAboutWindow.bind(this);
27 | this.onShowLogoutConfirmation = this.onShowLogoutConfirmation.bind(this);
28 | }
29 |
30 | onShowLogoutConfirmation() {
31 | this.props.showLogoutConfirmation({
32 | text: 'Log out?',
33 | onOk: (hideModal) => {
34 | this.props.doLogout(() => {
35 | hideModal();
36 | });
37 | }
38 | });
39 | }
40 |
41 | onShowAboutWindow() {
42 | this.props.showAboutWindow();
43 | }
44 |
45 | render() {
46 | return (
47 |
48 | }
56 | />
57 | }
58 | anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
59 | targetOrigin={{horizontal: 'right', vertical: 'top'}}
60 | >
61 | } />
62 | } onClick={this.onShowAboutWindow} />
63 | } onClick={this.onShowLogoutConfirmation} />
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | function mapDispatchToProps(dispatch) {
71 | return {
72 | showAboutWindow() {
73 | dispatch(showModal({key: AboutWindow.NAME}));
74 | },
75 | showLogoutConfirmation(props) {
76 | dispatch(showModal({key: ConfirmationDialog.NAME, props}));
77 | },
78 | doLogout(callback) {
79 | // get rid of callback here
80 | dispatch(apiGetServers(callback));
81 | setTimeout(() => {
82 | dispatch(showModal({key: AboutWindow.NAME}));
83 | }, 1500);
84 | }
85 | };
86 | }
87 |
88 | export default connect(
89 | null,
90 | mapDispatchToProps
91 | )(AppMenu);
92 |
--------------------------------------------------------------------------------
/src/client/containers/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {Link} from 'react-router-dom';
4 | import Dialog from 'material-ui/Dialog';
5 | import RaisedButton from 'material-ui/RaisedButton';
6 |
7 | export default class LoginForm extends React.Component {
8 | static propTypes = {
9 | children: PropTypes.node
10 | };
11 |
12 | static defaultProps = {
13 | children: ''
14 | };
15 |
16 | render() {
17 | const actions = [
18 |
19 | ,
20 |
21 | ];
22 |
23 | return (
24 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/client/containers/ModalsLayout/ModalsLayout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 |
5 | import {hideModal} from 'containers/ModalsLayout/actions';
6 | import AboutWindow from 'components/AboutWindow';
7 | import ConfirmationDialog from 'components/ConfirmationDialog';
8 | import ErrorWindow from 'components/ErrorWindow';
9 |
10 | const modalComponentList = [AboutWindow, ErrorWindow, ConfirmationDialog];
11 |
12 | export class ModalsLayout extends React.Component {
13 | static propTypes = {
14 | modals: PropTypes.object.isRequired,
15 | onHideModal: PropTypes.func.isRequired
16 | };
17 |
18 | constructor(...args) {
19 | super(...args);
20 |
21 | this._modalComponentsMap = {};
22 | modalComponentList.forEach((component) => {
23 | if (component.NAME) {
24 | this._modalComponentsMap[component.NAME] = component;
25 | } else {
26 | console.warn(`Component must have "NAME" property to be used as modal window: ${component}`);
27 | }
28 | });
29 | }
30 |
31 | renderModalComponent(key, props) {
32 | const onHideModal = this.props.onHideModal.bind(this, {key});
33 | return React.createElement(this._modalComponentsMap[key], {
34 | key,
35 | onHideModal,
36 | ...props
37 | });
38 | }
39 |
40 | render() {
41 | const {modals} = this.props;
42 | const children = [];
43 |
44 | for (const [key, props] of modals.entries()) {
45 | children.push(this.renderModalComponent(key, props));
46 | }
47 |
48 | return ;
49 | }
50 | }
51 |
52 | function mapStateToProps(state) {
53 | return {
54 | modals: state.modals
55 | };
56 | }
57 |
58 | function mapDispatchToProps(dispatch) {
59 | return {
60 | onHideModal(componentName) {
61 | dispatch(hideModal(componentName));
62 | }
63 | };
64 | }
65 |
66 | export default connect(
67 | mapStateToProps,
68 | mapDispatchToProps
69 | )(ModalsLayout);
70 |
--------------------------------------------------------------------------------
/src/client/containers/ModalsLayout/actions.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'redux-actions';
2 |
3 | export const SHOW_MODAL = 'SHOW_MODAL';
4 | export const showModal = createAction(SHOW_MODAL);
5 |
6 | export const REMOVE_MODAL = 'REMOVE_MODAL';
7 | export const removeModal = createAction(REMOVE_MODAL);
8 |
9 | export const ANIMATED_REMOVE_MODAL = 'ANIMATED_REMOVE_MODAL';
10 | const animatedRemoveModal = createAction(ANIMATED_REMOVE_MODAL);
11 |
12 | export function hideModal(payload) {
13 | return function animatedRemoveModalRunner(dispatch) {
14 | dispatch(animatedRemoveModal(payload));
15 | setTimeout(() => dispatch(removeModal(payload)), 500);
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/src/client/containers/ModalsLayout/reducer.js:
--------------------------------------------------------------------------------
1 | import {OrderedMap} from 'immutable';
2 |
3 | import {SHOW_MODAL, ANIMATED_REMOVE_MODAL, REMOVE_MODAL} from 'containers/ModalsLayout/actions';
4 |
5 | const initialState = OrderedMap({});
6 |
7 | export default function ModalsLayoutReducer(state = initialState, action) {
8 | switch (action.type) {
9 | case SHOW_MODAL:
10 | return state.set(action.payload.key, {
11 | ...action.payload.props,
12 | open: true
13 | });
14 | case ANIMATED_REMOVE_MODAL:
15 | return state.set(action.payload.key, {
16 | ...state.get(action.payload.key),
17 | ...action.payload.props,
18 | open: false
19 | });
20 | case REMOVE_MODAL:
21 | return state.delete(action.payload.key);
22 | default:
23 | return state;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/client/containers/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Header from 'components/Header/Header';
4 |
5 | export default function NotFound() {
6 | return (
7 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/client/containers/ServersPage/ServersPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {connect} from 'react-redux';
4 | import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
5 | import RaisedButton from 'material-ui/RaisedButton';
6 | import TextField from 'material-ui/TextField';
7 | import {FormattedMessage, FormattedRelative} from 'react-intl';
8 |
9 | import {apiAddServer, apiGetServers} from 'api/actions';
10 |
11 | import styles from './ServersPage.scss';
12 |
13 | export class ServersPage extends React.Component {
14 | static propTypes = {
15 | loading: PropTypes.bool.isRequired,
16 | servers: PropTypes.object.isRequired,
17 | apiAddServer: PropTypes.func.isRequired,
18 | apiGetServers: PropTypes.func.isRequired,
19 | serversLastUpdate: PropTypes.number
20 | };
21 |
22 | static defaultProps = {
23 | serversLastUpdate: null
24 | };
25 |
26 | componentDidMount() {
27 | this.props.apiGetServers();
28 | setTimeout(() => this.props.apiGetServers(), 1500);
29 | }
30 |
31 | render() {
32 | const {loading, servers, serversLastUpdate} = this.props;
33 |
34 | return (
35 |
36 |
37 |
38 | {serversLastUpdate && (
39 |
40 | (updated )
41 |
42 | )}
43 |
44 | {
47 | this.addServerTextFieldValue = value;
48 | }}
49 | hintText={}
50 | />
51 | }
54 | onClick={() => {
55 | this.props.apiAddServer({name: this.addServerTextFieldValue});
56 | }}
57 | />
58 |
59 |
60 |
61 | ID
62 | Name
63 |
64 |
65 |
66 | {servers.map(({id, name}) => (
67 |
68 | {id}
69 | {name}
70 |
71 | ))}
72 |
73 |
74 |
75 | );
76 | }
77 | }
78 |
79 | function mapStateToProps(state) {
80 | return {
81 | loading: state.api.get('loading'),
82 | servers: state.api.getIn(['data', 'servers']),
83 | serversLastUpdate: state.api.getIn(['lastUpdate', 'servers'])
84 | };
85 | }
86 |
87 | function mapDispatchToProps(dispatch) {
88 | return {
89 | apiGetServers() {
90 | dispatch(apiGetServers());
91 | },
92 | apiAddServer(data) {
93 | dispatch(apiAddServer(data));
94 | }
95 | };
96 | }
97 |
98 | export default connect(
99 | mapStateToProps,
100 | mapDispatchToProps
101 | )(ServersPage);
102 |
--------------------------------------------------------------------------------
/src/client/containers/ServersPage/ServersPage.scss:
--------------------------------------------------------------------------------
1 | .lastUpdate {
2 | color: gray;
3 | float: right;
4 | font-size: 10px;
5 | margin-top: 10px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | React + Express
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import '@babel/polyfill';
2 | import 'whatwg-fetch';
3 |
4 | import 'sanitize.css/sanitize.css';
5 |
6 | import intl from 'intl';
7 | import React from 'react';
8 | import ReactDOM from 'react-dom';
9 |
10 | import api from 'api/index';
11 | import App from './App';
12 |
13 | // global styles
14 | import './style.scss';
15 |
16 | // apply polyfill
17 | if (!window.Intl) {
18 | window.Intl = intl;
19 | }
20 |
21 | api.setEndpoint('/api');
22 |
23 | ReactDOM.render(, document.getElementById('app'));
24 |
--------------------------------------------------------------------------------
/src/client/logger.js:
--------------------------------------------------------------------------------
1 | import {Iterable} from 'immutable';
2 | import {createLogger} from 'redux-logger';
3 |
4 | const reduxActionsLogger = createLogger({
5 | collapsed: true,
6 | logErrors: true,
7 | // titleFormatter: ((action, time, took) => (`Action: ${String(action.type)} [${time} ${Math.round(took)}ms]`)),
8 | stateTransformer: (state) => {
9 | const newState = {};
10 | Object.keys(state).forEach((i) => {
11 | if (Iterable.isIterable(state[i])) {
12 | newState[i] = state[i].toJS();
13 | } else {
14 | newState[i] = state[i];
15 | }
16 | });
17 | return newState;
18 | }
19 | });
20 |
21 | export default reduxActionsLogger;
22 |
--------------------------------------------------------------------------------
/src/client/muiTheme.js:
--------------------------------------------------------------------------------
1 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
2 | import {green600, teal300, teal500, teal700} from 'material-ui/styles/colors';
3 |
4 | export default getMuiTheme({
5 | fontSize: 14,
6 | fontFamily: 'Comfortaa',
7 | palette: {
8 | primary1Color: teal700,
9 | primary2Color: teal500,
10 | primary3Color: teal300,
11 | accent1Color: green600
12 | },
13 | toolbar: {
14 | backgroundColor: teal700
15 | },
16 | flatButton: {
17 | textTransform: 'none',
18 | fontWeight: 'bold',
19 | fontSize: 15,
20 | buttonFilterColor: green600
21 | },
22 | button: {
23 | height: 36
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/src/client/reducers.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import {routerReducer} from 'react-router-redux';
3 |
4 | import ApiReducer from 'api/reducer';
5 | import ModalsLayoutReducer from 'containers/ModalsLayout/reducer';
6 |
7 | export default combineReducers({
8 | modals: ModalsLayoutReducer,
9 | routing: routerReducer,
10 | api: ApiReducer
11 | });
12 |
--------------------------------------------------------------------------------
/src/client/router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {BrowserRouter, Route, Switch} from 'react-router-dom';
3 |
4 | import AppLayout from 'containers/AppLayout';
5 | import LoginForm from 'containers/LoginForm';
6 |
7 | export default function() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/client/store.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware} from 'redux';
2 | import thunk from 'redux-thunk';
3 |
4 | import reducers from './reducers';
5 | import reduxActionsLogger from './logger';
6 |
7 | const initialState = {};
8 |
9 | const middlewares = [thunk];
10 |
11 | // dev debug
12 | if (module.hot) {
13 | middlewares.push(reduxActionsLogger);
14 | }
15 |
16 | export default createStore(reducers, initialState, applyMiddleware(...middlewares));
17 |
--------------------------------------------------------------------------------
/src/client/style.scss:
--------------------------------------------------------------------------------
1 | html, body {
2 | background-color: white;
3 | min-width: 700px;
4 | font-family: 'Comfortaa';
5 | }
6 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const setupApiRoutes = require('./middlewares/api');
5 | const logger = require('./logger');
6 |
7 | process.env.NODE_ENV = process.env.NODE_ENV || 'development';
8 | process.env.HTTP_PORT = process.env.HTTP_PORT || 3000;
9 |
10 | function onUnhandledError(err) {
11 | try {
12 | logger.error(err);
13 | } catch (e) {
14 | console.log('LOGGER ERROR:', e); //eslint-disable-line no-console
15 | console.log('APPLICATION ERROR:', err); //eslint-disable-line no-console
16 | }
17 | process.exit(1);
18 | }
19 |
20 | process.on('unhandledRejection', onUnhandledError);
21 | process.on('uncaughtException', onUnhandledError);
22 |
23 | const setupAppRoutes =
24 | process.env.NODE_ENV === 'development' ? require('./middlewares/development') : require('./middlewares/production');
25 |
26 | const app = express();
27 |
28 | app.set('env', process.env.NODE_ENV);
29 | logger.info(`Application env: ${process.env.NODE_ENV}`);
30 |
31 | app.use(logger.expressMiddleware);
32 | app.use(bodyParser.json());
33 |
34 | // application routes
35 | setupApiRoutes(app);
36 | setupAppRoutes(app);
37 |
38 | http.createServer(app).listen(process.env.HTTP_PORT, () => {
39 | logger.info(`HTTP server is now running on http://localhost:${process.env.HTTP_PORT}`);
40 | });
41 |
--------------------------------------------------------------------------------
/src/server/logger.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const {homedir} = require('os');
3 | const winston = require('winston');
4 | const jsonStringify = require('fast-safe-stringify');
5 |
6 | const LOG_FILE_NAME = '.application.log';
7 | const LOG_FILE_PATH =
8 | process.env.NODE_ENV === 'production'
9 | ? path.join(homedir(), LOG_FILE_NAME)
10 | : path.join(__dirname, '..', '..', LOG_FILE_NAME);
11 |
12 | const defaultFormats = [
13 | winston.format.timestamp(),
14 | winston.format.printf(
15 | ({timestamp, level, message}) =>
16 | `${timestamp} ${level}: ${typeof message === 'string' ? message : '\n' + jsonStringify(message, null, 4)}`
17 | )
18 | ];
19 |
20 | const logger = winston.createLogger({
21 | transports: [
22 | new winston.transports.Console({
23 | format: winston.format.combine(winston.format.colorize(), ...defaultFormats),
24 | level: process.env.LOG_LEVEL_CONSOLE,
25 | handleExceptions: true
26 | }),
27 | new winston.transports.File({
28 | format: winston.format.combine(winston.format.uncolorize(), ...defaultFormats),
29 | level: process.env.LOG_LEVEL_FILE,
30 | filename: LOG_FILE_PATH,
31 | handleExceptions: true,
32 | tailable: true,
33 | maxsize: 10 * 1024 * 1024,
34 | maxFiles: 5
35 | })
36 | ]
37 | });
38 |
39 | logger.expressMiddleware = function expressMiddleware(req, res, next) {
40 | if (req.url.includes('__webpack') && process.env.NODE_ENV === 'development') {
41 | return next();
42 | }
43 |
44 | const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
45 | const defaultMessage = `${ip} - ${req.method} ${req.url}`;
46 | const startTimestemp = Date.now();
47 | const waitingTimePrintInterval = 5000;
48 |
49 | let waitingTime = 0;
50 | const intervalId = setInterval(() => {
51 | waitingTime += waitingTimePrintInterval;
52 | logger.warn(`${defaultMessage} - wait for ${waitingTime / 1000}s...`);
53 | }, waitingTimePrintInterval);
54 |
55 | logger.info(defaultMessage);
56 |
57 | const printExecutionTime = (statusCode = '') => {
58 | const message = `${defaultMessage} - ${statusCode} - ${(Date.now() - startTimestemp) / 1000}s`;
59 | if (res.statusCode < 400) {
60 | logger.info(message);
61 | } else {
62 | logger.warn(message);
63 | }
64 | };
65 |
66 | req.on('end', () => {
67 | clearInterval(intervalId);
68 | if (!req.isProxy) {
69 | printExecutionTime(res.statusCode);
70 | }
71 | });
72 |
73 | req.on('close', () => {
74 | clearInterval(intervalId);
75 | if (!req.isProxy) {
76 | printExecutionTime('[closed by user]');
77 | }
78 | });
79 |
80 | return next();
81 | };
82 |
83 | logger.info(`Application logs file: ${LOG_FILE_PATH}`);
84 |
85 | module.exports = logger;
86 |
--------------------------------------------------------------------------------
/src/server/middlewares/api.js:
--------------------------------------------------------------------------------
1 | // API
2 |
3 | const servers = [{id: 1, name: 'a'}, {id: 2, name: 'b'}, {id: 3, name: 'c'}];
4 |
5 | module.exports = function setup(app) {
6 | app.get('/api/stats', (req, res) => {
7 | setTimeout(() => {
8 | res.json({
9 | // error: 'server error message',
10 | status: 'online',
11 | servers
12 | });
13 | }, 3000);
14 | });
15 |
16 | app.post('/api/servers', (req, res) => {
17 | if (!req.body.name) {
18 | return res.json({
19 | error: 'cannot add server with empty name'
20 | });
21 | }
22 | return setTimeout(() => {
23 | servers.push({
24 | id: servers[servers.length - 1].id + 1,
25 | name: req.body.name
26 | });
27 | res.json({
28 | success: true
29 | });
30 | }, 3000);
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/src/server/middlewares/development.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path');
2 | const webpack = require('webpack');
3 | const webpackDevMiddleware = require('webpack-dev-middleware');
4 | const webpackHotMiddleware = require('webpack-hot-middleware');
5 | const logger = require('../logger');
6 | const webpackConfig = require('../../../config/webpack.config.dev');
7 |
8 | const compiler = webpack(webpackConfig);
9 |
10 | module.exports = function setup(app) {
11 | app.use(
12 | webpackDevMiddleware(compiler, {
13 | logger,
14 | publicPath: webpackConfig.output.publicPath,
15 | stats: {
16 | colors: true
17 | }
18 | })
19 | );
20 |
21 | app.use(webpackHotMiddleware(compiler));
22 |
23 | // all other requests be handled by UI itself
24 | app.get('*', (req, res) => res.sendFile(resolve(__dirname, '..', '..', '..', 'build-dev', 'client', 'index.html')));
25 | };
26 |
--------------------------------------------------------------------------------
/src/server/middlewares/production.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path');
2 | const express = require('express');
3 | const compression = require('compression');
4 |
5 | const clientBuildPath = resolve(__dirname, '..', '..', 'client');
6 |
7 | module.exports = function setup(app) {
8 | app.use(compression());
9 | app.use('/', express.static(clientBuildPath));
10 |
11 | // all other requests be handled by UI itself
12 | app.get('*', (req, res) => res.sendFile(resolve(clientBuildPath, 'index.html')));
13 | };
14 |
--------------------------------------------------------------------------------