├── 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 |
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 |
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 |
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 ``
24 | // and ``.
25 | //
26 | // And also the URL for website "favicon".
27 | //
28 | assets: () => ({
29 | // Javascripts and (optionally) styles for the `entries`.
30 | // They are output by client-side Webpack build.
31 | // E.g.:
32 | // {
33 | // javascript: {
34 | // main: '/assets/main.js'
35 | // },
36 | // // (optional)
37 | // styles: {
38 | // main: '/assets/main.css'
39 | // }
40 | // }
41 | ...parameters.chunks(),
42 |
43 | // Website "favicon"
44 | icon
45 | }),
46 |
47 | // One can set `renderContent` flag to `false`
48 | // to turn off Server-Side React Rendering.
49 | // It only disables page rendering,
50 | // i.e. the inside of the `` DOM element
51 | // while everything around it is still
52 | // rendered on server side (e.g. `