├── configuration ├── configuration.development.json ├── configuration.production.json ├── setup │ ├── configuration.production.json │ ├── configuration.development.json │ ├── configuration.default.json │ └── index.js └── index.js ├── src ├── pages │ ├── Error.css │ ├── Home.css │ ├── NotFound.js │ ├── Error.js │ ├── Unauthorized.js │ ├── Unauthenticated.js │ ├── Home.js │ ├── Application.css │ ├── Users.css │ ├── Application.js │ └── Users.js ├── styles │ ├── common.css │ ├── style.css │ ├── components.css │ ├── react-responsive-ui.css │ ├── base.css │ ├── constants.css │ ├── grid.mixins.css │ └── grid.css ├── redux │ ├── reducers.js │ ├── notifications.js │ ├── reducers.with-hot-reload.js │ └── users.js ├── components │ ├── PageLoadingIndicator.js │ ├── Snackbar.js │ ├── LinearProgress.js │ ├── Menu.js │ ├── PageLoading.css │ ├── PageLoading.js │ ├── LinearProgress.css │ └── Menu.css ├── RootComponent.js ├── render.js ├── index.js ├── routes.js └── react-pages.js ├── api ├── users │ ├── list │ │ ├── function.json │ │ └── index.js │ ├── create │ │ ├── function.json │ │ └── index.js │ ├── get │ │ ├── function.json │ │ └── index.js │ ├── update │ │ ├── function.json │ │ └── index.js │ └── delete │ │ ├── function.json │ │ └── index.js ├── serverless.json ├── custom │ ├── onCall.js │ └── initialize.js └── index.js ├── assets └── images │ ├── husky.jpg │ ├── icon.png │ ├── home.svg │ └── users.svg ├── docs └── images │ └── screenshot.png ├── webpack ├── universal-webpack-settings.json ├── webpack.config.server.production.js ├── webpack.config.server.development.js ├── webpack.config.client.development.js ├── webpack.config.client.production.js ├── devserver.js └── webpack.config.js ├── nodemon.json ├── .postcssrc ├── rendering-service ├── index.js └── main.js ├── runnable └── create-commonjs-package-json.js ├── project.sublime-project ├── .gitignore ├── babel.config.js ├── proxy-server └── index.js ├── README.md └── package.json /configuration/configuration.development.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /configuration/configuration.production.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/pages/Error.css: -------------------------------------------------------------------------------- 1 | @import "../styles/common"; 2 | -------------------------------------------------------------------------------- /src/styles/common.css: -------------------------------------------------------------------------------- 1 | @import "./constants"; 2 | @import "./grid.mixins"; -------------------------------------------------------------------------------- /configuration/setup/configuration.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "webserver": { 3 | "port": 3000 4 | } 5 | } -------------------------------------------------------------------------------- /api/users/list/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-users", 3 | "path": "/example/users", 4 | "method": "GET" 5 | } -------------------------------------------------------------------------------- /configuration/setup/configuration.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "webpackDevServer": { 3 | "port": 3000 4 | } 5 | } -------------------------------------------------------------------------------- /api/users/create/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-user", 3 | "path": "/example/users", 4 | "method": "POST" 5 | } -------------------------------------------------------------------------------- /api/users/get/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-user", 3 | "path": "/example/users/{id}", 4 | "method": "GET" 5 | } -------------------------------------------------------------------------------- /api/users/update/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "update-user", 3 | "path": "/example/users", 4 | "method": "PATCH" 5 | } -------------------------------------------------------------------------------- /api/users/delete/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delete-user", 3 | "path": "/example/users/{id}", 4 | "method": "DELETE" 5 | } -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | export { default as users } from './users.js'; 2 | export { default as notifications } from './notifications.js'; -------------------------------------------------------------------------------- /assets/images/husky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/webpack-react-redux-server-side-render-example/HEAD/assets/images/husky.jpg -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/webpack-react-redux-server-side-render-example/HEAD/assets/images/icon.png -------------------------------------------------------------------------------- /api/serverless.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "code": { 4 | "initialize": "./custom/initialize.js", 5 | "onCall": "./custom/onCall.js" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catamphetamine/webpack-react-redux-server-side-render-example/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /src/styles/style.css: -------------------------------------------------------------------------------- 1 | @import "./constants"; 2 | @import "./react-responsive-ui"; 3 | @import "./components"; 4 | @import "./grid"; 5 | @import "./base"; 6 | -------------------------------------------------------------------------------- /configuration/setup/configuration.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "pageServer": { 3 | "port": 3001 4 | }, 5 | "api": { 6 | "port": 3002 7 | }, 8 | "publicPath": "/assets" 9 | } -------------------------------------------------------------------------------- /webpack/universal-webpack-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "input": "./rendering-service/main.js", 4 | "output": "./build/server/server.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/users/list/index.js: -------------------------------------------------------------------------------- 1 | export default async function() { 2 | const users = database.getCollection('users') 3 | if (!users) { 4 | return [] 5 | } 6 | return users.find() 7 | } -------------------------------------------------------------------------------- /api/custom/onCall.js: -------------------------------------------------------------------------------- 1 | async function $onCall() { 2 | await new Promise((resolve, reject) => { 3 | database.loadDatabase({}, (error) => error ? reject(error) : resolve()) 4 | }) 5 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | ".git", 4 | "node_modules/**/*" 5 | ], 6 | "env": { 7 | "NODE_ENV": "development" 8 | }, 9 | "legacyWatch": false, 10 | "verbose": true 11 | } 12 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": 3 | { 4 | "autoprefixer": {}, 5 | "postcss-import": {}, 6 | "postcss-mixins": {}, 7 | "postcss-simple-vars": {}, 8 | "postcss-nested": {}, 9 | "postcss-calc": {} 10 | } 11 | } -------------------------------------------------------------------------------- /rendering-service/index.js: -------------------------------------------------------------------------------- 1 | import startServer from 'universal-webpack/server' 2 | import settings from '../webpack/universal-webpack-settings.json' assert { type: 'json' } 3 | import configuration from '../webpack/webpack.config.js' 4 | 5 | startServer(configuration, settings) 6 | -------------------------------------------------------------------------------- /api/custom/initialize.js: -------------------------------------------------------------------------------- 1 | import loki from 'lokijs' 2 | 3 | function $initialize() { 4 | const database = new loki('database.json') 5 | global.database = database 6 | global.loadDatabase = function() { 7 | return new Promise(resolve => database.loadDatabase({}, resolve)) 8 | } 9 | } -------------------------------------------------------------------------------- /src/redux/notifications.js: -------------------------------------------------------------------------------- 1 | import { ReduxModule } from 'react-pages' 2 | 3 | const redux = new ReduxModule() 4 | 5 | export const notify = redux.simpleAction( 6 | (content, options) => ({ content, ...options }), 7 | 'notification' 8 | ) 9 | 10 | export default redux.reducer() -------------------------------------------------------------------------------- /webpack/webpack.config.server.production.js: -------------------------------------------------------------------------------- 1 | import { serverConfiguration } from 'universal-webpack' 2 | import settings from './universal-webpack-settings.json' assert { type: 'json' } 3 | import baseConfiguration from './webpack.config.js' 4 | 5 | export default serverConfiguration(baseConfiguration, settings) -------------------------------------------------------------------------------- /api/users/get/index.js: -------------------------------------------------------------------------------- 1 | import { NotFound } from 'serverless-functions/errors' 2 | 3 | export default async function({ params: { id } }) { 4 | const users = database.getCollection('users') 5 | if (!users || !users.by('id', id)) { 6 | throw new NotFound(`User ${id} not found`) 7 | } 8 | return users.by('id', id) 9 | } -------------------------------------------------------------------------------- /src/pages/Home.css: -------------------------------------------------------------------------------- 1 | @import "../styles/common"; 2 | 3 | .home-page-image 4 | { 5 | display : block; 6 | max-width : 100%; 7 | 8 | margin-left : auto; 9 | margin-right : auto; 10 | 11 | border-width : 1px; 12 | border-style : solid; 13 | border-color : #7f7f7f; 14 | 15 | border-radius : 0.5em; 16 | } -------------------------------------------------------------------------------- /src/components/PageLoadingIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useLoading } from 'react-pages' 3 | 4 | import PageLoading from './PageLoading.js' 5 | 6 | export default function PageLoadingIndicator() { 7 | const isLoading = useLoading() 8 | return ( 9 | 10 | ) 11 | } -------------------------------------------------------------------------------- /src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './Error.css' 4 | 5 | export default function NotFound() { 6 | return ( 7 |
8 |

9 | Page not found 10 |

11 |
12 | ) 13 | } 14 | 15 | NotFound.meta = () => ({ title: 'Not found' }) -------------------------------------------------------------------------------- /webpack/webpack.config.server.development.js: -------------------------------------------------------------------------------- 1 | import configuration from './webpack.config.server.production.js' 2 | import { setDevFileServer } from './devserver.js' 3 | 4 | // Same as production configuration 5 | // with the only change that all files 6 | // are served by webpack devserver. 7 | export default setDevFileServer(configuration) 8 | -------------------------------------------------------------------------------- /api/users/delete/index.js: -------------------------------------------------------------------------------- 1 | import { NotFound } from 'serverless-functions/errors' 2 | 3 | export default async function({ params: { id } }) { 4 | const users = database.getCollection('users') 5 | if (!users.by('id', id)) { 6 | throw new NotFound(`User ${id} not found`) 7 | } 8 | users.findAndRemove({ id }) 9 | database.saveDatabase() 10 | } -------------------------------------------------------------------------------- /src/pages/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './Error.css' 4 | 5 | export default function ErrorPage() { 6 | return ( 7 |
8 |

9 | Some kind of an error happened 10 |

11 |
12 | ) 13 | } 14 | 15 | ErrorPage.meta = () => ({ title: 'Error' }) -------------------------------------------------------------------------------- /src/styles/components.css: -------------------------------------------------------------------------------- 1 | .page-header 2 | { 3 | margin-top : 0; 4 | text-align : center; 5 | } 6 | 7 | .page-content 8 | { 9 | margin-top : calc(var(--unit) * 2); 10 | margin-bottom : calc(var(--unit) * 4); 11 | 12 | @mixin xs 13 | { 14 | margin-top : calc(var(--unit) * 1); 15 | margin-bottom : calc(var(--unit) * 2); 16 | } 17 | } -------------------------------------------------------------------------------- /src/RootComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Provider } from 'react-redux' 4 | 5 | export default function RootComponent({ store, children }) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | 13 | RootComponent.propTypes = { 14 | store: PropTypes.object.isRequired 15 | } -------------------------------------------------------------------------------- /src/redux/reducers.with-hot-reload.js: -------------------------------------------------------------------------------- 1 | import { updateReducers } from 'react-pages' 2 | 3 | import * as reducers from './reducers.js' 4 | 5 | export * from './reducers.js' 6 | 7 | // Enables hot-reload via Webpack "Hot Module Replacement". 8 | if (import.meta.webpackHot) { 9 | import.meta.webpackHot.accept(['./reducers.js'], () => { 10 | updateReducers(reducers) 11 | }) 12 | } -------------------------------------------------------------------------------- /src/pages/Unauthorized.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './Error.css' 4 | 5 | export default function Unauthorized() { 6 | return ( 7 |
8 |

9 | You're not authorized to perform this action 10 |

11 |
12 | ) 13 | } 14 | 15 | Unauthorized.meta = () => ({ title: 'Unauthorized' }) -------------------------------------------------------------------------------- /src/pages/Unauthenticated.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './Error.css' 4 | 5 | export default function Unauthenticated() { 6 | return ( 7 |
8 |

9 | You need to sign in to access this page 10 |

11 |
12 | ) 13 | } 14 | 15 | Unauthenticated.meta = () => ({ title: 'Unauthenticated' }) -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import { render } from 'react-pages/client' 2 | 3 | import settings from './react-pages.js' 4 | 5 | export default async function() { 6 | // Renders the webpage on the client side 7 | const { enableHotReload } = await render(settings) 8 | 9 | // Enables hot-reload via Webpack "Hot Module Replacement". 10 | if (import.meta.webpackHot) { 11 | enableHotReload() 12 | } 13 | } -------------------------------------------------------------------------------- /api/users/update/index.js: -------------------------------------------------------------------------------- 1 | import { NotFound } from 'serverless-functions/errors' 2 | 3 | export default async function({ params: { id }, body: { name } }) { 4 | const users = database.getCollection('users') 5 | if (!users || !users.by('id', id)) { 6 | throw new NotFound(`User ${id} not found`) 7 | } 8 | users.findAndUpdate({ id }, user => user.name = name) 9 | database.saveDatabase() 10 | } -------------------------------------------------------------------------------- /runnable/create-commonjs-package-json.js: -------------------------------------------------------------------------------- 1 | // Creates a `package.json` file in the CommonJS `build` folder. 2 | // That marks that whole folder as CommonJS so that Node.js doesn't complain 3 | // about `require()`-ing those files. 4 | 5 | import fs from 'fs' 6 | 7 | fs.writeFileSync('./build/package.json', JSON.stringify({ 8 | name: 'application/build', 9 | type: 'commonjs', 10 | private: true 11 | }, null, 2), 'utf8') 12 | -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "follow_symlinks": true, 6 | "path": ".", 7 | "file_exclude_patterns": ["npm-debug.log", "*.js.map"], 8 | "folder_exclude_patterns": ["node_modules", "log", "build"] 9 | } 10 | ], 11 | "settings": 12 | { 13 | "tab_size": 2, 14 | "translate_tabs_to_spaces": false, 15 | "ensure_newline_at_eof_on_save": false, 16 | "trim_trailing_white_space_on_save": true 17 | } 18 | } -------------------------------------------------------------------------------- /src/components/Snackbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | 4 | import { Snackbar } from 'react-responsive-ui' 5 | // Webpack still can't learn how to "tree-shake" ES6 imports. 6 | // import Snackbar from 'react-responsive-ui/commonjs/Snackbar' 7 | 8 | export default function SnackBar() { 9 | const notification = useSelector(state => state.notifications.notification) 10 | return ( 11 | 13 | ) 14 | } -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import husky from '../../assets/images/husky.jpg' 4 | 5 | import './Home.css' 6 | 7 | export default function HomePage() { 8 | return ( 9 |
10 |

11 | Husky 12 |

13 | 14 | 17 |
18 | ) 19 | } 20 | 21 | HomePage.meta = () => { 22 | return { 23 | title: 'Home' 24 | } 25 | } -------------------------------------------------------------------------------- /api/users/create/index.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import { InputRejected } from 'serverless-functions/errors' 3 | 4 | export default async function({ body: { name } }) { 5 | if (!name) { 6 | throw new InputRejected(`"name" is required`) 7 | } 8 | const id = uuidv4() 9 | let users = database.getCollection('users') 10 | if (!users) { 11 | users = database.addCollection('users', { unique: ['id'] }) 12 | } 13 | users.insert({ id, name, dateAdded: new Date() }) 14 | database.saveDatabase() 15 | return id 16 | } -------------------------------------------------------------------------------- /src/components/LinearProgress.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | 5 | import './LinearProgress.css' 6 | 7 | export default function LinearProgress({ className }) { 8 | return ( 9 |
10 |
11 |
12 |
13 | ) 14 | } 15 | 16 | LinearProgress.propTypes = { 17 | className: PropTypes.string 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-pages' 3 | import classNames from 'classnames' 4 | 5 | import './Menu.css' 6 | 7 | export default function Menu({ className, children }) { 8 | return ( 9 |
    10 | {children} 11 |
12 | ) 13 | } 14 | 15 | export function MenuLink({ to, exact, children }) { 16 | return ( 17 |
  • 18 | 23 | {children} 24 | 25 |
  • 26 | ) 27 | } -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import { run } from 'serverless-functions' 2 | 3 | import setupConfig from '../configuration/setup/index.js' 4 | import serverlessConfig from './serverless.json' assert { type: 'json' } 5 | 6 | // https://ru.stackoverflow.com/questions/1281148/referenceerror-dirname-is-not-defined 7 | import { fileURLToPath } from 'url'; 8 | import { dirname } from 'path'; 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | run('dev', setupConfig.api.port, serverlessConfig, { cwd: __dirname }).then(() => { 13 | console.info(`API is listening at http://localhost:${setupConfig.api.port}`) 14 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # maybe some logs 2 | /log 3 | 4 | # webpack build target folder 5 | /build 6 | 7 | # npm modules 8 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 9 | /node_modules 10 | 11 | # bower, but nobody uses it since we've got webpack 12 | bower_components/ 13 | 14 | # npm errors 15 | npm-debug.log 16 | 17 | # github pages 18 | gh-pages/ 19 | 20 | # for OS X users 21 | .DS_Store 22 | 23 | # cache files for sublime text 24 | *.tmlanguage.cache 25 | *.tmPreferences.cache 26 | *.stTheme.cache 27 | 28 | # workspace files are user-specific 29 | *.sublime-workspace 30 | 31 | # test coverage folder 32 | coverage 33 | 34 | # database 35 | database.json -------------------------------------------------------------------------------- /src/styles/react-responsive-ui.css: -------------------------------------------------------------------------------- 1 | @import "react-responsive-ui/style"; 2 | @import "./grid.mixins"; 3 | 4 | @import "react-responsive-ui/small-screen/Modal.css" (max-width: $screen-sm-min); 5 | @import "react-responsive-ui/small-screen/Snackbar.css" (max-width: $screen-sm-min); 6 | @import "react-responsive-ui/small-screen/DatePicker.InputOverlay.css" (max-width: $screen-sm-min); 7 | 8 | :root { 9 | --rrui-unit : var(--unit); 10 | --rrui-white-color : var(--white-color); 11 | --rrui-black-color : var(--black-color); 12 | --rrui-accent-color : var(--base-color); 13 | --rrui-accent-color-light : var(--base-color-lighter); 14 | --rrui-gray-color : var(--gray-color); 15 | } 16 | 17 | .rrui__snackbar--error { 18 | background-color: #cc0000; 19 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // `core-js` and `regenerator-runtime` would've been imported here 2 | // in case of using `useBuiltIns: 'entry'` option of `@babel/preset-env` 3 | // https://stackoverflow.com/questions/52625979/confused-about-usebuiltins-option-of-babel-preset-env-using-browserslist-integ 4 | // https://babeljs.io/docs/en/babel-preset-env 5 | // 6 | // When using `useBuiltIns: 'auto'`, importing `core-js` and `regenerator-runtime` 7 | // explicitly is not required, and Babel adds those automatically. 8 | // 9 | // // ES6 polyfill. 10 | // import 'core-js/stable' 11 | // // `async/await` support. 12 | // import 'regenerator-runtime/runtime' 13 | 14 | // Maintain CSS styles order. 15 | import './styles/style.css' 16 | 17 | // Run the application. 18 | import render from './render.js' 19 | 20 | render().catch((error) => console.error(error)) -------------------------------------------------------------------------------- /assets/images/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | html, body 2 | { 3 | /* Removes higlight on tap on mobile devices. */ 4 | -webkit-tap-highlight-color : transparent; 5 | } 6 | 7 | body 8 | { 9 | /* Disables "double tap to zoom in" on mobile devices. */ 10 | /* https://stackoverflow.com/questions/46167604/iphone-html-disable-double-tap-to-zoom */ 11 | touch-action: manipulation; 12 | } 13 | 14 | body, input, textarea, select 15 | { 16 | font-family : sans-serif; 17 | font-size : var(--font-size); 18 | 19 | @mixin xs 20 | { 21 | font-size : var(--font-size-xs); 22 | } 23 | } 24 | 25 | body 26 | { 27 | margin : 0; 28 | overflow-y : scroll; 29 | } 30 | 31 | a 32 | { 33 | color: inherit; 34 | } 35 | 36 | a:active 37 | { 38 | color: var(--base-color-darker); 39 | } 40 | 41 | /* Internet Explorer adds borders around all images */ 42 | img 43 | { 44 | border: none; 45 | } -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import Application from './pages/Application.js' 2 | 3 | import Users from './pages/Users.js' 4 | import Home from './pages/Home.js' 5 | 6 | import GenericError from './pages/Error.js' 7 | import Unauthenticated from './pages/Unauthenticated.js' 8 | import Unauthorized from './pages/Unauthorized.js' 9 | import NotFound from './pages/NotFound.js' 10 | 11 | export default [{ 12 | path: '/', 13 | Component: Application, 14 | children: [ 15 | { Component: Home }, 16 | { Component: Users, path: 'users' }, 17 | { Component: Unauthenticated, path: 'unauthenticated', status: 401 }, 18 | { Component: Unauthenticated, path: 'unauthenticated', status: 401 }, 19 | { Component: Unauthorized, path: 'unauthorized', status: 403 }, 20 | { Component: NotFound, path: 'not-found', status: 404 }, 21 | { Component: GenericError, path: 'error', status: 500 }, 22 | { Component: NotFound, path: '*', status: 404 } 23 | ] 24 | }] -------------------------------------------------------------------------------- /src/components/PageLoading.css: -------------------------------------------------------------------------------- 1 | .PageLoading { 2 | position: fixed; 3 | 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | 9 | z-index: -1; 10 | opacity: 0; 11 | 12 | transition: background-color 0ms linear var(--PageLoading-hideAnimationDuration), opacity var(--PageLoading-hideAnimationDuration) ease-out, z-index var(--PageLoading-hideAnimationDuration) step-end; 13 | } 14 | 15 | .PageLoading--show { 16 | opacity: 1; 17 | cursor: wait; 18 | z-index: var(--PageLoading-zIndex); 19 | transition: opacity var(--PageLoading-showAnimationDuration) ease-out var(--PageLoading-showAnimationDelay), z-index 0ms step-start; 20 | } 21 | 22 | .PageLoading--showImmediately { 23 | transition-delay: 0ms; 24 | } 25 | 26 | .PageLoading--show.PageLoading--initial { 27 | background-color: var(--PageLoading-backgroundColor--initial); 28 | } 29 | 30 | .PageLoading--show:not(.PageLoading--initial) { 31 | background-color: var(--PageLoading-backgroundColor); 32 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const REACT_FAST_REFRESH_PLUGINS = [] 2 | 3 | // Works around `react-refresh-webpack-plugin` bug: 4 | // "$RefreshReg$ is not defined". 5 | // Another wokraround: 6 | // https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/176#issuecomment-782770175 7 | if (process.env.NODE_ENV === 'development') { 8 | REACT_FAST_REFRESH_PLUGINS.push('react-refresh/babel') 9 | } 10 | 11 | export default { 12 | presets: [ 13 | "@babel/preset-env" 14 | ], 15 | plugins: [ 16 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 17 | "@babel/plugin-syntax-import-assertions" 18 | ], 19 | overrides: [{ 20 | include: "./src", 21 | presets: [ 22 | "@babel/preset-react" 23 | ], 24 | plugins: [ 25 | ["babel-plugin-transform-react-remove-prop-types", { removeImport: true }], 26 | ...REACT_FAST_REFRESH_PLUGINS 27 | ] 28 | }, { 29 | include: "./rendering-service", 30 | presets: [ 31 | "@babel/preset-react" 32 | ] 33 | }] 34 | } -------------------------------------------------------------------------------- /src/pages/Application.css: -------------------------------------------------------------------------------- 1 | @import "../styles/common"; 2 | 3 | /* Stretches the page to 100% height */ 4 | .webpage 5 | { 6 | /* For z-index ordering (relative to preloading screen) */ 7 | position : relative; 8 | z-index : 0; 9 | 10 | display : flex; 11 | flex-direction : column; 12 | min-height : 100vh; 13 | } 14 | 15 | .webpage--loading 16 | { 17 | /* 18 | `react-waypoint` wouldn't work correctly with `display: none` -> `display: block`. 19 | https://github.com/brigade/react-waypoint/issues/164#issuecomment-299640438 20 | display: none; 21 | */ 22 | visibility: hidden; 23 | } 24 | 25 | /* Content takes all free space */ 26 | .webpage__content 27 | { 28 | flex-shrink : 0; 29 | flex-grow : 1; 30 | flex-basis : auto; 31 | } 32 | 33 | .webpage__header 34 | { 35 | margin-top : calc(var(--unit) * 2); 36 | padding-top : calc(var(--unit) * 2); 37 | padding-bottom : calc(var(--unit) * 2); 38 | 39 | /* For XS screens. */ 40 | @mixin xs 41 | { 42 | margin-top : 0; 43 | } 44 | } -------------------------------------------------------------------------------- /src/pages/Users.css: -------------------------------------------------------------------------------- 1 | @import "../styles/common"; 2 | 3 | .users__description 4 | { 5 | margin-top : 0; 6 | } 7 | 8 | .users__refresh 9 | { 10 | margin-left : 2rem; 11 | } 12 | 13 | .users__content 14 | { 15 | margin-top : 1.5rem; 16 | } 17 | 18 | .users__list 19 | { 20 | border-collapse : collapse; 21 | } 22 | 23 | .users__list td 24 | { 25 | padding-top : 0.25em; 26 | padding-bottom : 0.25em; 27 | padding-left : 0.5em; 28 | padding-right : 0.5em; 29 | } 30 | 31 | .users__list td:first-child 32 | { 33 | padding-left : 0; 34 | } 35 | 36 | .users__list tr:first-child td 37 | { 38 | padding-top : 0; 39 | } 40 | 41 | .user__id 42 | { 43 | color : #9f9f9f; 44 | text-align : center; 45 | } 46 | 47 | .user__delete 48 | { 49 | height : auto; 50 | } 51 | 52 | .add-user 53 | { 54 | padding : 2rem; 55 | } 56 | 57 | .add-user__name, 58 | .add-user__submit 59 | { 60 | display : inline-block; 61 | vertical-align : top; 62 | } 63 | 64 | .add-user__name 65 | { 66 | width : auto; 67 | margin-right : 1rem; 68 | } -------------------------------------------------------------------------------- /src/redux/users.js: -------------------------------------------------------------------------------- 1 | import { ReduxModule } from 'react-pages' 2 | 3 | const redux = new ReduxModule() 4 | 5 | export const getUsers = redux.action( 6 | 'GET_USERS', 7 | () => async http => { 8 | await delay(1000) 9 | return await http.get('api://example/users') 10 | }, 11 | 'users' 12 | ) 13 | 14 | export const addUser = redux.action( 15 | 'ADD_USER', 16 | (user) => async http => { 17 | await delay(1500) 18 | await http.post('api://example/users', user) 19 | } 20 | ) 21 | 22 | export const deleteUser = redux.action( 23 | // Action name is optional. 24 | // Will be autogenerated if not passed. 25 | // 'DELETE_USER', 26 | (id) => async http => { 27 | await delay(1000) 28 | await http.delete(`api://example/users/${id}`) 29 | } 30 | ) 31 | 32 | const initialState = { users: [] } 33 | 34 | // This is the Redux reducer which now 35 | // handles the asynchronous actions defined above. 36 | export default redux.reducer(initialState) 37 | 38 | // "Sleep" using `Promise` 39 | const delay = (delay) => new Promise(resolve => setTimeout(resolve, delay)) -------------------------------------------------------------------------------- /assets/images/users.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/PageLoading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useSelector } from 'react-redux' 4 | import classNames from 'classnames' 5 | import { FadeInOut } from 'react-responsive-ui' 6 | 7 | import LinearProgress from './LinearProgress.js' 8 | 9 | import './PageLoading.css' 10 | 11 | export default function PageLoading({ 12 | initial, 13 | show, 14 | showAnimationDelay, 15 | hideAnimationDuration 16 | }) { 17 | return ( 18 |
    24 | 25 | 26 | 27 |
    28 | ) 29 | } 30 | 31 | PageLoading.propTypes = { 32 | initial: PropTypes.bool, 33 | show: PropTypes.bool, 34 | hideAnimationDuration: PropTypes.number.isRequired, 35 | showAnimationDelay: PropTypes.number 36 | } 37 | 38 | PageLoading.defaultProps = { 39 | hideAnimationDuration: 160 40 | } -------------------------------------------------------------------------------- /configuration/setup/index.js: -------------------------------------------------------------------------------- 1 | // import { merge } from 'lodash-es' 2 | import merge from 'lodash/merge.js' 3 | 4 | import defaultConfiguration from './configuration.default.json' assert { type: 'json' } 5 | import productionConfiguration from './configuration.production.json' assert { type: 'json' } 6 | import developmentConfiguration from './configuration.development.json' assert { type: 'json' } 7 | 8 | const configuration = merge({}, defaultConfiguration) 9 | 10 | export default configuration 11 | 12 | merge(configuration, getConfiguration(process.env.NODE_ENV)) 13 | 14 | // For services like Amazon Elastic Compute Cloud and Heroku 15 | if (process.env.PORT) { 16 | configuration.webserver.port = process.env.PORT 17 | } 18 | 19 | // For passing custom configuration via an environment variable. 20 | // For frameworks like Docker. 21 | // E.g. `CONFIGURATION="{ \"key\": \"value\" }" npm start`. 22 | if (process.env.CONFIGURATION) { 23 | try { 24 | merge(configuration, JSON.parse(process.env.CONFIGURATION)) 25 | } catch (error) { 26 | console.error(error) 27 | } 28 | } 29 | 30 | function getConfiguration(env) { 31 | switch (env) { 32 | case 'production': 33 | return productionConfiguration 34 | default: 35 | return developmentConfiguration 36 | } 37 | } -------------------------------------------------------------------------------- /src/styles/constants.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --white-color : #ffffff; 3 | --black-color : #000000; 4 | 5 | --gray-color : #888C91; 6 | 7 | --base-color-lighter : #30CFFF; 8 | --base-color : #00A6ED; 9 | --base-color-darker : #0084BD; 10 | 11 | --unit : 0.7rem; 12 | --column-gap : var(--unit); 13 | --page-content-padding : calc(var(--column-gap) * 2); 14 | 15 | /* Mobile font size must be at least 16px 16 | to prevent automatic zoom on input focus. */ 17 | --font-size : 18px; 18 | --font-size-xs : 16px; 19 | 20 | /* PageLoading. */ 21 | --PageLoading-hideAnimationDuration: 160ms; 22 | --PageLoading-showAnimationDuration: 600ms; 23 | --PageLoading-showAnimationDelay: 500ms; 24 | --PageLoading-zIndex: 10; 25 | --PageLoading-backgroundColor: rgba(0,0,0,0.08); 26 | --PageLoading-backgroundColor--initial: #ffffff; 27 | 28 | /* LinearProgress. */ 29 | --LinearProgress-backgroundColor: rgb(167, 202, 237); 30 | --LinearProgress-color: #1976d2; 31 | --LinearProgress-height: 4px; 32 | --LinearProgress-animationTimingFactor: 1; 33 | --LinearProgress-animationDuration: calc(2.1s * var(--LinearProgress-animationTimingFactor)); 34 | --LinearProgress-animationInterval: calc(1.18s * var(--LinearProgress-animationTimingFactor)); 35 | } -------------------------------------------------------------------------------- /webpack/webpack.config.client.development.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | 3 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin' 4 | 5 | import { clientConfiguration } from 'universal-webpack' 6 | import settings from './universal-webpack-settings.json' assert { type: 'json' } 7 | import baseConfiguration from './webpack.config.js' 8 | 9 | import { devServerConfig, setDevFileServer } from './devserver.js' 10 | 11 | let configuration = clientConfiguration(baseConfiguration, settings) 12 | 13 | // `webpack-serve` can't set the correct `mode` by itself 14 | // so setting `mode` to `"development"` explicitly. 15 | // https://github.com/webpack-contrib/webpack-serve/issues/94 16 | configuration.mode = 'development' 17 | 18 | // Only when using `webpack-dev-server`. 19 | if (process.env.SERVE) { 20 | configuration.plugins.push(new ReactRefreshWebpackPlugin()) 21 | } 22 | 23 | // Fetch all files from webpack development server. 24 | configuration = setDevFileServer(configuration) 25 | 26 | // Run `webpack serve`. 27 | configuration.devServer = devServerConfig 28 | 29 | // Prints more readable module names in the browser console on HMR updates. 30 | configuration.optimization = { 31 | ...configuration.optimization, 32 | moduleIds: 'named' 33 | } 34 | 35 | export default configuration -------------------------------------------------------------------------------- /proxy-server/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webservice from 'web-service' 3 | 4 | import setupConfig from '../configuration/setup/index.js' 5 | 6 | // https://ru.stackoverflow.com/questions/1281148/referenceerror-dirname-is-not-defined 7 | import { fileURLToPath } from 'url'; 8 | import { dirname } from 'path'; 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | const webserver = webservice({}) 13 | 14 | // Serve static files 15 | webserver.files('/assets', path.join(__dirname, '../build/assets')) 16 | 17 | // if it's not a static file url: 18 | 19 | // Proxy `/api` requests to API server. 20 | // Wouldn't do it in a real-world app 21 | // and would just query the API directly 22 | // but Chrome won't allow that for `localhost`. 23 | webserver.proxy('/api', `http://localhost:${setupConfig.api.port}`, { name: 'API service' }) 24 | 25 | // Proxy all the rest requests to Webpage rendering server. 26 | webserver.proxy(`http://localhost:${setupConfig.pageServer.port}`, { name: 'Page rendering service' }) 27 | 28 | // Start web server 29 | webserver.listen(setupConfig.webserver.port).then(() => 30 | { 31 | console.info(`Web server is listening`) 32 | console.info(`Now go to http://localhost:${setupConfig.webserver.port}`) 33 | }, 34 | (error) => 35 | { 36 | console.error(error) 37 | }) 38 | -------------------------------------------------------------------------------- /configuration/index.js: -------------------------------------------------------------------------------- 1 | // This is the "public" configuration of the app. 2 | // It's embedded in the bundle so don't put any secret keys here. 3 | 4 | import setupConfiguration from './setup/index.js' 5 | 6 | import productionConfiguration from './configuration.production.json' assert { type: 'json' } 7 | import developmentConfiguration from './configuration.development.json' assert { type: 'json' } 8 | 9 | const configuration = getConfiguration(process.env.NODE_ENV) 10 | 11 | // API service absolute URL. 12 | // 13 | // Chrome won't allow querying `localhost` from `localhost` 14 | // so had to just proxy the `/api` path using `webpack-serve`. 15 | // 16 | // The Chrome error was: 17 | // 18 | // "Failed to load http://localhost:3003/example/users: 19 | // Response to preflight request doesn't pass access control check: 20 | // No 'Access-Control-Allow-Origin' header is present on the requested resource. 21 | // Origin 'http://localhost:3000' is therefore not allowed access." 22 | // 23 | // https://stackoverflow.com/a/10892392/970769 24 | // 25 | configuration.api = `${setupConfiguration.api.secure ? 'https' : 'http'}://${setupConfiguration.api.host || 'localhost'}:${setupConfiguration.api.port}` 26 | 27 | export default configuration 28 | 29 | function getConfiguration(env) { 30 | switch (env) { 31 | case 'production': 32 | return productionConfiguration 33 | default: 34 | return developmentConfiguration 35 | } 36 | } -------------------------------------------------------------------------------- /src/components/LinearProgress.css: -------------------------------------------------------------------------------- 1 | /* Copy-pasted from `material-ui/LinearProgress`. */ 2 | /* https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/LinearProgress/LinearProgress.js */ 3 | /* https://material-ui.com/components/progress/ */ 4 | 5 | .LinearProgress { 6 | position: relative; 7 | height: var(--LinearProgress-height); 8 | overflow: hidden; 9 | background-color: var(--LinearProgress-backgroundColor) 10 | } 11 | 12 | .LinearProgress-bar { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | bottom: 0; 17 | transition: transform 0.2s linear; 18 | transform-origin: left; 19 | background-color: var(--LinearProgress-color); 20 | } 21 | 22 | .LinearProgress-bar--1 { 23 | animation: MuiLinearProgress-keyframes-indeterminate1 var(--LinearProgress-animationDuration) cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 24 | } 25 | 26 | .LinearProgress-bar--2 { 27 | animation: MuiLinearProgress-keyframes-indeterminate2 var(--LinearProgress-animationDuration) cubic-bezier(0.165, 0.84, 0.44, 1) var(--LinearProgress-animationInterval) infinite; 28 | } 29 | 30 | @keyframes MuiLinearProgress-keyframes-indeterminate1 { 31 | /* |-----|---x-||-----||-----| */ 32 | 0% { 33 | left: -35%; 34 | right: 100%; 35 | } 36 | /* |-----|-----||-----||xxxx-| */ 37 | 60% { 38 | left: 100%; 39 | right: -90%; 40 | } 41 | 100% { 42 | left: 100%; 43 | right: -90%; 44 | } 45 | } 46 | 47 | @keyframes MuiLinearProgress-keyframes-indeterminate2 { 48 | /* |xxxxx|xxxxx||-----||-----| */ 49 | 0% { 50 | left: -200%; 51 | right: 100%; 52 | } 53 | /* |-----|-----||-----||-x----| */ 54 | 60% { 55 | left: 107%; 56 | right: -8%; 57 | } 58 | 100% { 59 | left: 107%; 60 | right: -8%; 61 | } 62 | } -------------------------------------------------------------------------------- /webpack/webpack.config.client.production.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | import { CleanWebpackPlugin } from 'clean-webpack-plugin' 4 | // import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' 5 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 6 | import TerserPlugin from 'terser-webpack-plugin' 7 | 8 | import { clientConfiguration } from 'universal-webpack' 9 | import settings from './universal-webpack-settings.json' assert { type: 'json' } 10 | import baseConfiguration from './webpack.config.js' 11 | 12 | const configuration = clientConfiguration(baseConfiguration, settings, { 13 | // Extract all CSS into separate `*.css` files (one for each chunk) 14 | // using `mini-css-extract-plugin` 15 | // instead of leaving that CSS embedded directly in `*.js` chunk files. 16 | development: false, 17 | useMiniCssExtractPlugin: true 18 | }) 19 | 20 | configuration.devtool = 'source-map' 21 | 22 | // Minimize CSS. 23 | // https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production 24 | configuration.optimization = { 25 | minimizer: [ 26 | new TerserPlugin({ 27 | parallel: true 28 | }), 29 | new CssMinimizerPlugin() 30 | ] 31 | }; 32 | 33 | configuration.plugins.push( 34 | // Clears the output folder before building. 35 | new CleanWebpackPlugin(), 36 | 37 | // Use `--analyze` CLI option of webpack instead. 38 | // // Shows the resulting bundle size stats (too). 39 | // // https://github.com/webpack-contrib/webpack-bundle-analyzer 40 | // new BundleAnalyzerPlugin({ 41 | // // The path is relative to the output folder 42 | // reportFilename : '../bundle-stats-2.html', 43 | // analyzerMode : 'static', 44 | // openAnalyzer : false 45 | // }) 46 | ) 47 | 48 | export default configuration 49 | -------------------------------------------------------------------------------- /src/pages/Application.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | 5 | // `react-time-ago` English language. 6 | import JavascriptTimeAgo from 'javascript-time-ago' 7 | import en from 'javascript-time-ago/locale/en' 8 | JavascriptTimeAgo.addLocale(en) 9 | 10 | import Menu, { MenuLink } from '../components/Menu.js' 11 | import Snackbar from '../components/Snackbar.js' 12 | import PageLoadingIndicator from '../components/PageLoadingIndicator.js' 13 | 14 | import Home from '../../assets/images/home.svg' 15 | import Users from '../../assets/images/users.svg' 16 | 17 | import './Application.css' 18 | 19 | export default function App({ children }) { 20 | return ( 21 |
    22 | {/* Page loading indicator */} 23 | 24 | 25 | {/* Pop-up messages */} 26 | 27 | 28 |
    29 | 43 | 44 |
    45 | {children} 46 |
    47 | 48 |
    49 | {/* */} 50 |
    51 |
    52 |
    53 | ) 54 | } 55 | 56 | App.propTypes = { 57 | children: PropTypes.node.isRequired 58 | } 59 | 60 | // Default ``. 61 | App.meta = () => { 62 | return { 63 | site_name : 'WebApp', 64 | title : 'WebApp', 65 | description : 'A generic web application boilerplate', 66 | image : 'https://www.google.ru/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png', 67 | locale : 'en_US', 68 | locales : ['ru_RU', 'en_US'] 69 | } 70 | } -------------------------------------------------------------------------------- /webpack/devserver.js: -------------------------------------------------------------------------------- 1 | import setupConfig from '../configuration/setup/index.js' 2 | 3 | const PORT = setupConfig.webpackDevServer.port 4 | 5 | // `webpack serve` settings. 6 | export const devServerConfig = { 7 | // The port to serve assets on. 8 | port: PORT, 9 | 10 | static: { 11 | directory: setupConfig.publicPath + '/' 12 | }, 13 | 14 | // Chrome won't allow querying `localhost` from `localhost` 15 | // so had to just proxy the `/api` path using `webpack serve`. 16 | // 17 | // The Chrome error was: 18 | // 19 | // "Failed to load http://localhost:3003/example/users: 20 | // Response to preflight request doesn't pass access control check: 21 | // No 'Access-Control-Allow-Origin' header is present on the requested resource. 22 | // Origin 'http://localhost:3000' is therefore not allowed access." 23 | // 24 | // https://stackoverflow.com/a/10892392/970769 25 | // 26 | proxy: [{ 27 | context: (path) => { 28 | return path !== '/api' && path.indexOf('/api/') !== 0 29 | }, 30 | target: `http://localhost:${setupConfig.pageServer.port}` 31 | }, { 32 | context: '/api', 33 | target: `${setupConfig.api.secure ? 'https' : 'http'}://${setupConfig.api.host || 'localhost'}:${setupConfig.api.port}`, 34 | pathRewrite: { '^/api' : '' } 35 | }], 36 | 37 | // // This is just for forcing `webpack serve` 38 | // // to not disable proxying for root path (`/`). 39 | // index: '', 40 | 41 | // Uncomment if using `index.html` instead of Server-Side Rendering. 42 | // https://webpack.js.org/configuration/dev-server/#devserver-historyapifallback 43 | // historyApiFallback: true, 44 | 45 | headers: { 46 | 'Access-Control-Allow-Origin': '*' 47 | } 48 | } 49 | 50 | // Modifies webpack configuration to get all files 51 | // from webpack development server. 52 | export function setDevFileServer(configuration) { 53 | return { 54 | ...configuration, 55 | output: { 56 | ...configuration.output, 57 | publicPath: `http://localhost:${PORT}${configuration.output.publicPath}` 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/styles/grid.mixins.css: -------------------------------------------------------------------------------- 1 | /* https://www.sitepoint.com/managing-responsive-breakpoints-sass/ */ 2 | 3 | /* 4 | // `calc()` won't work here because these variables are used inside mixins. 5 | // https://github.com/postcss/postcss-mixins/issues/86 6 | // 7 | // $grid-unit : 12px; 8 | // 9 | // $screen-sm-min : calc($grid-unit * 64); // 768px 10 | // $screen-xs-max : calc($screen-sm-min - 1); 11 | // $screen-md-min : calc($grid-unit * 82); // 984px 12 | // $screen-sm-max : calc($screen-md-min - 1); 13 | // $screen-lg-min : calc($grid-unit * 100); // 1200px 14 | // $screen-md-max : calc($screen-lg-min - 1); 15 | // 16 | */ 17 | $screen-sm-min : 768px; 18 | $screen-xs-max : 767px; 19 | $screen-md-min : 984px; 20 | $screen-sm-max : 983px; 21 | $screen-lg-min : 1200px; 22 | $screen-md-max : 1199px; 23 | 24 | @define-mixin xs 25 | { 26 | @media all and (max-width: $screen-xs-max) 27 | { 28 | @mixin-content; 29 | } 30 | } 31 | 32 | @define-mixin s 33 | { 34 | @media all and (min-width: $screen-sm-min) and (max-width: $screen-sm-max) 35 | { 36 | @mixin-content; 37 | } 38 | } 39 | 40 | @define-mixin m 41 | { 42 | @media all and (min-width: $screen-md-min) and (max-width: $screen-md-max) 43 | { 44 | @mixin-content; 45 | } 46 | } 47 | 48 | @define-mixin l 49 | { 50 | @media all and (min-width: $screen-lg-min) 51 | { 52 | @mixin-content; 53 | } 54 | } 55 | 56 | @define-mixin xs-s 57 | { 58 | @media all and (max-width: $screen-sm-max) 59 | { 60 | @mixin-content; 61 | } 62 | } 63 | 64 | @define-mixin xs-m 65 | { 66 | @media all and (max-width: $screen-md-max) 67 | { 68 | @mixin-content; 69 | } 70 | } 71 | 72 | @define-mixin s-m 73 | { 74 | @media all and (min-width: $screen-sm-min) and (max-width: $screen-md-max) 75 | { 76 | @mixin-content; 77 | } 78 | } 79 | 80 | @define-mixin m-l 81 | { 82 | @media all and (min-width: $screen-md-min) 83 | { 84 | @mixin-content; 85 | } 86 | } 87 | 88 | @define-mixin s-l 89 | { 90 | @media all and (min-width: $screen-sm-min) 91 | { 92 | @mixin-content; 93 | } 94 | } -------------------------------------------------------------------------------- /src/components/Menu.css: -------------------------------------------------------------------------------- 1 | @import "../styles/common"; 2 | 3 | .menu 4 | { 5 | margin : 0; 6 | padding : 0; 7 | list-style-type : none; 8 | } 9 | 10 | .menu-list-item 11 | { 12 | display : inline-flex; 13 | align-items : center; 14 | 15 | margin-left : calc(var(--unit) * 2); 16 | margin-right : calc(var(--unit) * 2); 17 | 18 | @mixin xs 19 | { 20 | margin-left : calc(var(--unit) * 1); 21 | margin-right : calc(var(--unit) * 1); 22 | } 23 | 24 | &:first-child 25 | { 26 | margin-left : 0; 27 | } 28 | 29 | &:last-child 30 | { 31 | margin-right : 0; 32 | } 33 | } 34 | 35 | .menu-item 36 | { 37 | display : inline-flex; 38 | align-items : baseline; 39 | 40 | padding-bottom : 0; 41 | 42 | border-bottom-width : 0.12em; 43 | border-bottom-color : transparent; 44 | border-bottom-style : solid; 45 | 46 | text-decoration : none; 47 | 48 | &:first-child 49 | { 50 | margin-left : 0; 51 | } 52 | 53 | &:last-child 54 | { 55 | margin-right : 0; 56 | } 57 | } 58 | 59 | .menu-item--selected 60 | { 61 | border-bottom-color : var(--base-color); 62 | } 63 | 64 | .menu-item__icon 65 | { 66 | position : relative; 67 | bottom : -0.1em; 68 | 69 | width : calc(var(--unit) * 2.5); 70 | height : calc(var(--unit) * 2.5); 71 | 72 | margin-right : var(--unit); 73 | 74 | @mixin xs 75 | { 76 | width : calc(var(--unit) * 1.5); 77 | height : calc(var(--unit) * 1.5); 78 | 79 | margin-right : calc(var(--unit) * 0.5); 80 | } 81 | } 82 | 83 | .menu-item__icon--home 84 | { 85 | path 86 | { 87 | stroke : var(--black-color); 88 | fill : var(--black-color); 89 | } 90 | } 91 | 92 | .menu-item__icon--users 93 | { 94 | path, circle 95 | { 96 | stroke : var(--black-color); 97 | } 98 | } 99 | 100 | .menu-item--selected 101 | { 102 | .menu-item__icon--home 103 | { 104 | path 105 | { 106 | stroke : var(--base-color); 107 | fill : var(--base-color); 108 | } 109 | } 110 | 111 | .menu-item__icon--users 112 | { 113 | path, circle 114 | { 115 | stroke : var(--base-color); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /rendering-service/main.js: -------------------------------------------------------------------------------- 1 | import webpageServer from 'react-pages/server' 2 | 3 | import settings, { icon } from '../src/react-pages.js' 4 | import setupConfig from '../configuration/setup/index.js' 5 | 6 | export default function(parameters) { 7 | // Create webpage rendering server 8 | const server = webpageServer(settings, { 9 | // Proxy all HTTP requests for data 10 | // through a proxy server to the API server. 11 | // Wouldn't do such a thing in a real-world app 12 | // and would just query the API server directly 13 | // but Chrome won't allow that for `localhost`. 14 | proxy: { 15 | host: setupConfig.api.host || 'localhost', 16 | port: setupConfig.api.port, 17 | // For HTTPS 18 | secure: setupConfig.api.secure 19 | }, 20 | 21 | // HTTP URLs for javascripts and (optionally) CSS styles 22 | // which will be insterted into the `` element 23 | // of the resulting HTML webpage as `