├── .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 | [![Build Status](https://travis-ci.org/antonfisher/react-express-webpack.svg?branch=master)](https://travis-ci.org/antonfisher/react-express-webpack) 4 | [![GitHub license](https://img.shields.io/github/license/antonfisher/react-express-webpack.svg)](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 | ![Demo view](https://raw.githubusercontent.com/antonfisher/react-express-webpack/docs/images/rew2-ui-screenshot.png) 27 | 28 | Development `.js` bundles: 29 | 30 | ![Development js bundles](https://raw.githubusercontent.com/antonfisher/react-express-webpack/docs/images/rew-stat-dev.png) 31 | 32 | Production `.js` bundles: 33 | 34 | ![Production js bundles](https://raw.githubusercontent.com/antonfisher/react-express-webpack/docs/images/rew-stat-prod.png) 35 | 36 | Production mode server output: 37 | 38 | ![Production js bundles](https://raw.githubusercontent.com/antonfisher/react-express-webpack/docs/images/rew-log-prod.png) 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 | 37 | {text} 38 | 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 | 27 | {children} 28 | 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 | 25 |
26 | Login fields 27 | {React.Children.toArray(this.props.children)} 28 |
29 |
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
{children}
; 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 |
8 |
Page not found.
9 |
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 | --------------------------------------------------------------------------------