├── .gitignore
├── README.md
├── spa
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── client
│ ├── .eslintrc.json
│ ├── api
│ │ ├── config.js
│ │ ├── posts
│ │ │ ├── getAll.js
│ │ │ └── getTop5.js
│ │ └── session
│ │ │ ├── create.js
│ │ │ ├── destroy.js
│ │ │ └── local.js
│ ├── assets
│ │ ├── fonts
│ │ │ ├── latoLatinLight.ttf
│ │ │ ├── latoLatinLight.woff
│ │ │ ├── latoLatinLight.woff2
│ │ │ ├── nexaExtraboldWebfont.ttf
│ │ │ ├── nexaExtraboldWebfont.woff
│ │ │ ├── nexaExtraboldWebfont.woff2
│ │ │ ├── nexaHeavyWebfont.ttf
│ │ │ ├── nexaHeavyWebfont.woff
│ │ │ └── nexaHeavyWebfont.woff2
│ │ ├── img
│ │ │ └── peerigonLogoMint.svg
│ │ └── public
│ │ │ ├── favicon.ico
│ │ │ ├── manifest.json
│ │ │ ├── postImage1.jpg
│ │ │ ├── postImage2.jpg
│ │ │ ├── postImage3.jpg
│ │ │ ├── postImage4.jpg
│ │ │ └── userImage.jpg
│ ├── components
│ │ ├── about
│ │ │ ├── about.css.js
│ │ │ └── about.js
│ │ ├── allPosts
│ │ │ └── allPosts.js
│ │ ├── app
│ │ │ ├── app.css.js
│ │ │ └── app.js
│ │ ├── form
│ │ │ └── form.js
│ │ ├── formFeedback
│ │ │ ├── formFeedback.css.js
│ │ │ └── formFeedback.js
│ │ ├── header
│ │ │ ├── common.js
│ │ │ ├── header.css.js
│ │ │ ├── header.js
│ │ │ ├── link.css.js
│ │ │ ├── logo
│ │ │ │ ├── logo.css.js
│ │ │ │ └── logo.js
│ │ │ ├── nav
│ │ │ │ ├── nav.css.js
│ │ │ │ └── nav.js
│ │ │ └── session
│ │ │ │ ├── anonymous
│ │ │ │ └── anonymous.js
│ │ │ │ ├── personal
│ │ │ │ ├── personal.css.js
│ │ │ │ └── personal.js
│ │ │ │ └── session.js
│ │ ├── loading
│ │ │ ├── loading.css.js
│ │ │ └── loading.js
│ │ ├── loginForm
│ │ │ ├── loginForm.css.js
│ │ │ ├── loginForm.js
│ │ │ └── loginFormValidators.js
│ │ ├── modal
│ │ │ ├── modal.css.js
│ │ │ └── modal.js
│ │ ├── notFound
│ │ │ └── notFound.js
│ │ ├── posts
│ │ │ ├── post
│ │ │ │ ├── post.css.js
│ │ │ │ └── post.js
│ │ │ ├── posts.css.js
│ │ │ └── posts.js
│ │ ├── router
│ │ │ ├── goBack.js
│ │ │ ├── link.js
│ │ │ ├── routePlaceholder.js
│ │ │ ├── router.js
│ │ │ └── util
│ │ │ │ ├── routeToHref.js
│ │ │ │ ├── routingContext.js
│ │ │ │ └── trigger.js
│ │ ├── top5
│ │ │ └── top5.js
│ │ └── util
│ │ │ ├── placeholder.js
│ │ │ ├── withContext.js
│ │ │ └── withTitle.js
│ ├── index.html
│ ├── index.js
│ ├── init
│ │ ├── globals.js
│ │ ├── render.js
│ │ ├── serviceWorker.js
│ │ └── styles.js
│ ├── routes.js
│ ├── styles
│ │ ├── a11y.js
│ │ ├── block
│ │ │ ├── inputSubmit.js
│ │ │ ├── inputText.js
│ │ │ └── sheet.js
│ │ ├── borders.js
│ │ ├── calc.js
│ │ ├── colors.js
│ │ ├── gradients.js
│ │ ├── layout.js
│ │ ├── paddings.js
│ │ ├── pre
│ │ │ ├── body.css
│ │ │ └── reset.css
│ │ ├── scales.js
│ │ ├── timing.js
│ │ ├── type
│ │ │ ├── fonts
│ │ │ │ ├── latoLight.css
│ │ │ │ ├── nexaHeavy.css
│ │ │ │ └── nexaXbold.css
│ │ │ ├── latoLight.js
│ │ │ ├── nexaHeavy.js
│ │ │ └── nexaXBold.js
│ │ ├── typoSizes.js
│ │ └── zIndex.js
│ └── util
│ │ ├── asyncContext.js
│ │ ├── asyncPropsCache.js
│ │ ├── createEventHandler.js
│ │ ├── generateId.js
│ │ ├── htmlEntities.js
│ │ ├── mapToObject.js
│ │ └── useDefault.js
├── config
│ ├── server.json
│ └── webpack.config.babel.js
├── package-lock.json
├── package.json
├── server
│ ├── api.js
│ ├── dummyData
│ │ ├── generate.js
│ │ ├── posts.json
│ │ └── users.json
│ ├── env.js
│ └── index.js
└── tools
│ └── webpack
│ ├── InlinePreStylesPlugin.js
│ └── exportCssLoader.js
└── universal
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .vscode
└── launch.json
├── app
├── .eslintrc.json
├── assets
│ ├── fonts
│ │ ├── latoLatinLight.ttf
│ │ ├── latoLatinLight.woff
│ │ ├── latoLatinLight.woff2
│ │ ├── nexaExtraboldWebfont.ttf
│ │ ├── nexaExtraboldWebfont.woff
│ │ ├── nexaExtraboldWebfont.woff2
│ │ ├── nexaHeavyWebfont.ttf
│ │ ├── nexaHeavyWebfont.woff
│ │ └── nexaHeavyWebfont.woff2
│ ├── img
│ │ └── peerigonLogoMint.svg
│ └── public
│ │ ├── favicon.ico
│ │ ├── manifest.json
│ │ ├── postImage1.jpg
│ │ ├── postImage2.jpg
│ │ ├── postImage3.jpg
│ │ ├── postImage4.jpg
│ │ └── userImage.jpg
├── client
│ ├── .eslintrc.json
│ ├── captureFormSubmit.js
│ ├── captureHistoryPop.js
│ ├── captureLinkClick.js
│ ├── index.js
│ └── preloadChunkEntries.js
├── components
│ ├── about
│ │ ├── about.css.js
│ │ └── about.js
│ ├── allPosts
│ │ └── allPosts.js
│ ├── app
│ │ ├── app.css.js
│ │ └── app.js
│ ├── chunks
│ │ ├── chunks.js
│ │ └── defineChunkEntry.js
│ ├── document
│ │ └── document.js
│ ├── error
│ │ ├── error.css.js
│ │ └── error.js
│ ├── form
│ │ ├── defineForm.js
│ │ └── form.js
│ ├── formFeedback
│ │ ├── formFeedback.css.js
│ │ └── formFeedback.js
│ ├── header
│ │ ├── common.js
│ │ ├── header.css.js
│ │ ├── header.js
│ │ ├── link.css.js
│ │ ├── logo
│ │ │ ├── logo.css.js
│ │ │ └── logo.js
│ │ ├── nav
│ │ │ ├── nav.css.js
│ │ │ └── nav.js
│ │ └── session
│ │ │ ├── anonymous
│ │ │ └── anonymous.js
│ │ │ ├── personal
│ │ │ ├── personal.css.js
│ │ │ └── personal.js
│ │ │ └── session.js
│ ├── loading
│ │ ├── loading.css.js
│ │ └── loading.js
│ ├── loginForm
│ │ ├── index.js
│ │ ├── loginForm.css.js
│ │ ├── loginForm.js
│ │ └── loginFormValidators.js
│ ├── modal
│ │ ├── modal.css.js
│ │ ├── modal.js
│ │ ├── modalLink.js
│ │ └── modalTrigger.js
│ ├── placeholder
│ │ └── placeholder.js
│ ├── posts
│ │ ├── post
│ │ │ ├── post.css.js
│ │ │ └── post.js
│ │ ├── posts.css.js
│ │ └── posts.js
│ ├── router
│ │ ├── errors
│ │ │ └── methodNotAllowed.js
│ │ ├── link.js
│ │ ├── routePlaceholder.js
│ │ ├── router.js
│ │ └── util
│ │ │ ├── changeRoute.js
│ │ │ ├── createRouter.js
│ │ │ ├── enterRoute.js
│ │ │ ├── resolveRouteAndParams.js
│ │ │ └── sanitizeRequest.js
│ ├── session
│ │ └── session.js
│ ├── store
│ │ └── store.js
│ ├── top5
│ │ └── top5.js
│ └── util
│ │ ├── attrSelector.js
│ │ ├── defineComponent.js
│ │ ├── hookIntoEvent.js
│ │ ├── renderChild.js
│ │ └── withContext.js
├── contexts.js
├── createApp.js
├── effects
│ ├── .eslintrc.json
│ ├── api
│ │ ├── api.browser.js
│ │ ├── api.node.js
│ │ ├── index.js
│ │ ├── posts
│ │ │ ├── getAll.js
│ │ │ └── getTop5.js
│ │ └── session
│ │ │ ├── create.js
│ │ │ └── destroy.js
│ ├── csrf
│ │ ├── csrf.browser.js
│ │ ├── csrf.node.js
│ │ └── index.js
│ ├── document
│ │ ├── document.browser.js
│ │ ├── document.node.js
│ │ └── index.js
│ ├── history
│ │ ├── history.browser.js
│ │ ├── history.node.js
│ │ └── index.js
│ └── session
│ │ ├── index.js
│ │ ├── session.browser.js
│ │ └── session.node.js
├── env.js
├── routes
│ ├── about
│ │ ├── about.js
│ │ └── index.js
│ ├── allPosts
│ │ ├── allPosts.js
│ │ └── index.js
│ ├── error
│ │ ├── error.js
│ │ └── index.js
│ ├── index.js
│ ├── notFound
│ │ ├── index.js
│ │ └── notFound.js
│ ├── session
│ │ ├── index.js
│ │ └── session.js
│ └── top5
│ │ ├── index.js
│ │ └── top5.js
├── server
│ ├── assetTags.js
│ ├── createRenderStream.js
│ ├── index.js
│ ├── paths.js
│ ├── preloadAllChunkEntries.js
│ └── renderApp.js
├── store
│ ├── createReducer.js
│ ├── createStore.js
│ ├── defineState.js
│ ├── effectMiddleware.js
│ ├── enhanceStore.js
│ └── thunkMiddleware.js
├── styles
│ ├── a11y.js
│ ├── block
│ │ ├── inputSubmit.js
│ │ ├── inputText.js
│ │ └── sheet.js
│ ├── borders.js
│ ├── calc.js
│ ├── colors.js
│ ├── gradients.js
│ ├── layout.js
│ ├── paddings.js
│ ├── reset.js
│ ├── scales.js
│ ├── timing.js
│ ├── type
│ │ ├── latoLight.js
│ │ ├── nexaHeavy.js
│ │ └── nexaXBold.js
│ ├── typoSizes.js
│ └── zIndex.js
└── util
│ ├── addObjectKeys.js
│ ├── filterProps.js
│ ├── has.js
│ ├── htmlEntities.js
│ ├── renderUrl.js
│ └── statusCodes.js
├── config
├── server.json
└── webpack.config.babel.js
├── package-lock.json
├── package.json
├── server
├── api.js
├── config.js
├── dummyData
│ ├── generate.js
│ ├── posts.json
│ └── users.json
├── env.js
└── index.js
└── tools
└── webpack
├── ResolveEffectPlugin.js
├── WriteAssetsJsonPlugin.js
└── exportCssLoader.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Evaluation
2 |
3 | ## Platform recommendations
4 |
5 | The setup has been verified to run correctly with:
6 |
7 | - macOS Sierra 10.12.6
8 | - Node.js v8.1.3
9 | - NPM v5.0.3
10 | - Chrome Browser 60.0.3112.90
11 |
12 | **Please note: Only NPM 5 will install the exact same dependencies as specified in the `package-lock.json`. Older NPM versions might install different versions than the verified test setup.**
13 |
14 | ## SPA implementation
15 |
16 | ### Installation
17 |
18 | 1. Inside the `spa` folder, execute `npm install` to install all dependencies
19 | 2. Run `npm start`
20 | 3. Open [http://localhost:3000](http://localhost:3000)
21 | 4. Use "jhnns" and "password" as login credentials
22 |
23 | ## Universal implementation
24 |
25 | ### Installation
26 |
27 | 1. Inside the `universal` folder, execute `npm install` to install all dependencies
28 | 2. Run `npm start`
29 | 3. Open [http://localhost:3000](http://localhost:3000)
30 | 4. Use "jhnns" and "password" as login credentials
31 |
32 | ### Note on folder structure
33 |
34 | - `app` contains all the code that is going to be processed by the bundler.
35 | - `app/client` represents the client app entry.
36 | - `app/server` represents the server app entry.
37 | - `app/components` contains components, views and states. They have been coalesced by features. The folder name "components" refers here to its original meaning "self-contained part of a larger entity".
38 | - `app/routes` contains route handlers.
39 | - `server` is the server entry.
40 | - `server/api` roughly equals the server data layer.
--------------------------------------------------------------------------------
/spa/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "development": {
4 | "presets": [
5 | [
6 | "env",
7 | {
8 | "targets": "current"
9 | }
10 | ]
11 | ],
12 | "plugins": [
13 | "dynamic-import-node"
14 | ],
15 | "sourceMaps": "inline"
16 | },
17 | "browser": {
18 | "presets": [
19 | [
20 | "env",
21 | {
22 | "targets": {
23 | "browsers": [
24 | "last 2 versions"
25 | ]
26 | },
27 | "modules": false
28 | }
29 | ]
30 | ],
31 | "plugins": [
32 | [
33 | "transform-react-jsx",
34 | {
35 | "pragma": "h"
36 | }
37 | ],
38 | "transform-react-constant-elements"
39 | ]
40 | }
41 | },
42 | "plugins": [
43 | "syntax-dynamic-import",
44 | "transform-runtime",
45 | "transform-object-rest-spread"
46 | ],
47 | "retainLines": true
48 | }
--------------------------------------------------------------------------------
/spa/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "peerigon"
4 | ],
5 | "env": {
6 | "node": true
7 | },
8 | "root": true,
9 | "rules": {
10 | "no-mixed-operators": "off"
11 | }
12 | }
--------------------------------------------------------------------------------
/spa/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | /public
61 | /dist
--------------------------------------------------------------------------------
/spa/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "jsx-a11y"
4 | ],
5 | "extends": [
6 | "peerigon/react",
7 | "plugin:jsx-a11y/recommended"
8 | ],
9 | "env": {
10 | "browser": true
11 | },
12 | "settings": {
13 | "import/resolver": {
14 | "webpack": {
15 | "config": "config/webpack.config.babel.js"
16 | }
17 | }
18 | },
19 | "rules": {
20 | // Since we have no store, each React component may host its own state.
21 | // So, we don't use stateless functions in this SPA example to avoid unnecessary refactoring.
22 | // Currently, they provide not performance benefit anyway.
23 | "react/prefer-stateless-function": "off",
24 | "class-methods-use-this": "off",
25 | "react/no-unknown-property": [
26 | "error",
27 | {
28 | "ignore": [
29 | "class"
30 | ]
31 | }
32 | ]
33 | }
34 | }
--------------------------------------------------------------------------------
/spa/client/api/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | root: "/api",
3 | };
4 |
--------------------------------------------------------------------------------
/spa/client/api/posts/getAll.js:
--------------------------------------------------------------------------------
1 | import fetch from "unfetch";
2 | import config from "../config";
3 |
4 | export default function getAll() {
5 | return fetch(`${ config.root }/posts`)
6 | .then(res => res.json())
7 | .then(res => res.items);
8 | }
9 |
--------------------------------------------------------------------------------
/spa/client/api/posts/getTop5.js:
--------------------------------------------------------------------------------
1 | import fetch from "unfetch";
2 | import config from "../config";
3 |
4 | export default function getTop5() {
5 | return fetch(`${ config.root }/posts?limit=5&sortBy=starred`)
6 | .then(res => res.json())
7 | .then(res => res.items);
8 | }
9 |
--------------------------------------------------------------------------------
/spa/client/api/session/create.js:
--------------------------------------------------------------------------------
1 | import fetch from "unfetch";
2 | import { update as updateLocalSession } from "./local";
3 | import config from "../config";
4 |
5 | const defaultOptions = {
6 | method: "POST",
7 | headers: {
8 | "Content-Type": "application/json",
9 | },
10 | };
11 |
12 | export default function create(payload) {
13 | return fetch(`${ config.root }/session`, {
14 | ...defaultOptions,
15 | body: JSON.stringify(payload),
16 | })
17 | .then(res => res.json())
18 | .then(res => {
19 | if (res.status === "success") {
20 | updateLocalSession(res.data);
21 |
22 | return res.data;
23 | }
24 |
25 | throw new Error(res.message);
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/spa/client/api/session/destroy.js:
--------------------------------------------------------------------------------
1 | import { update as updateLocalSession } from "./local";
2 |
3 | export default function destroy() {
4 | updateLocalSession({
5 | token: null,
6 | user: null,
7 | });
8 | // Only a hard reload ensures that all personal data is cleared
9 | window.location.reload();
10 | }
11 |
--------------------------------------------------------------------------------
/spa/client/api/session/local.js:
--------------------------------------------------------------------------------
1 | const localStorageNamespace = "session";
2 | const session = deserialize() || {
3 | user: null,
4 | token: null,
5 | };
6 |
7 | function serialize() {
8 | localStorage.setItem(localStorageNamespace, JSON.stringify(session));
9 | }
10 |
11 | function deserialize() {
12 | const sessionString = localStorage.getItem(localStorageNamespace);
13 |
14 | if (sessionString === null) {
15 | return null;
16 | }
17 |
18 | return JSON.parse(sessionString);
19 | }
20 |
21 | export function update(newValues) {
22 | Object.keys(newValues).forEach(key => {
23 | session[key] = newValues[key];
24 | });
25 | serialize();
26 | }
27 |
28 | export default session;
29 |
--------------------------------------------------------------------------------
/spa/client/assets/fonts/latoLatinLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/latoLatinLight.ttf
--------------------------------------------------------------------------------
/spa/client/assets/fonts/latoLatinLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/latoLatinLight.woff
--------------------------------------------------------------------------------
/spa/client/assets/fonts/latoLatinLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/latoLatinLight.woff2
--------------------------------------------------------------------------------
/spa/client/assets/fonts/nexaExtraboldWebfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/nexaExtraboldWebfont.ttf
--------------------------------------------------------------------------------
/spa/client/assets/fonts/nexaExtraboldWebfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/nexaExtraboldWebfont.woff
--------------------------------------------------------------------------------
/spa/client/assets/fonts/nexaExtraboldWebfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/nexaExtraboldWebfont.woff2
--------------------------------------------------------------------------------
/spa/client/assets/fonts/nexaHeavyWebfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/nexaHeavyWebfont.ttf
--------------------------------------------------------------------------------
/spa/client/assets/fonts/nexaHeavyWebfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/nexaHeavyWebfont.woff
--------------------------------------------------------------------------------
/spa/client/assets/fonts/nexaHeavyWebfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/fonts/nexaHeavyWebfont.woff2
--------------------------------------------------------------------------------
/spa/client/assets/img/peerigonLogoMint.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
11 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/spa/client/assets/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/public/favicon.ico
--------------------------------------------------------------------------------
/spa/client/assets/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/spa/client/assets/public/postImage1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/public/postImage1.jpg
--------------------------------------------------------------------------------
/spa/client/assets/public/postImage2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/public/postImage2.jpg
--------------------------------------------------------------------------------
/spa/client/assets/public/postImage3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/public/postImage3.jpg
--------------------------------------------------------------------------------
/spa/client/assets/public/postImage4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/public/postImage4.jpg
--------------------------------------------------------------------------------
/spa/client/assets/public/userImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/spa/client/assets/public/userImage.jpg
--------------------------------------------------------------------------------
/spa/client/components/about/about.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import sheet from "../../styles/block/sheet";
4 | import {
5 | regularFontSize,
6 | regularLineHeight,
7 | regularMaxWidth,
8 | headlineFontSize,
9 | headlineLineHeight,
10 | } from "../../styles/typoSizes";
11 | import nexaHeavy from "../../styles/type/nexaHeavy";
12 | import latoLight from "../../styles/type/latoLight";
13 |
14 | export const root = css({
15 | position: "relative",
16 | });
17 |
18 | export const aboutSheet = css({
19 | ...sheet,
20 | maxWidth: regularMaxWidth + "rem",
21 | marginLeft: "auto",
22 | marginRight: "auto",
23 | });
24 |
25 | export const title = css({
26 | ...nexaHeavy,
27 | fontSize: headlineFontSize + "rem",
28 | lineHeight: headlineLineHeight + "rem",
29 | ":not(:last-child)": {
30 | marginBottom: rem(10) + "rem",
31 | },
32 | });
33 |
34 | export const text = css({
35 | ...latoLight,
36 | fontSize: regularFontSize + "rem",
37 | lineHeight: regularLineHeight + "rem",
38 | });
39 |
--------------------------------------------------------------------------------
/spa/client/components/about/about.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import { aboutSheet, title, text } from "./about.css";
3 |
4 | export default class About extends Component {
5 | render() {
6 | return (
7 |
8 |
9 |
About
10 |
11 |
12 | Delectus quia nulla sit ex ipsum sit animi incidunt. Nam rerum reiciendis et. Minus
13 | voluptatem natus mollitia temporibus. Molestias dolorem omnis eveniet repudiandae corporis
14 | voluptas sed quo.
15 |
16 |
17 | Quisquam a vel quia in quis blanditiis sed. Labore ratione minus. A quo consequuntur
18 | recusandae consequatur. Et aspernatur quod officia rem quam nisi vel est quidem.
19 |
20 |
21 |
22 | Alias et fugit error quaerat consequatur. Voluptatem omnis aut voluptatem. Et necessitatibus
23 | qui voluptatem.
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/spa/client/components/allPosts/allPosts.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import getAllPosts from "../../api/posts/getAll";
3 | import Posts from "../posts/posts";
4 | import WithTitle from "../util/withTitle";
5 |
6 | export default class AllPosts extends Component {
7 | render() {
8 | const title = "All Peerigon News";
9 |
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/spa/client/components/app/app.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { mintLight35, silverLight10, black } from "../../styles/colors";
3 | import { linear } from "../../styles/gradients";
4 | import { maxContentWidth } from "../../styles/layout";
5 | import { paddingRegular } from "../../styles/paddings";
6 |
7 | export const root = css({
8 | margin: 0,
9 | color: black(),
10 | backgroundImage: linear("to bottom", [silverLight10(), mintLight35() + " 70vh"]),
11 | minHeight: "100vh",
12 | });
13 |
14 | export const main = css({
15 | maxWidth: maxContentWidth + "rem",
16 | marginLeft: "auto",
17 | marginRight: "auto",
18 | ["@media (min-width: " + paddingRegular * 20 + "px)"]: {
19 | padding: paddingRegular,
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/spa/client/components/app/app.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Header from "../header/header";
3 | import Router from "../router/router";
4 | import RoutePlaceholder from "../router/routePlaceholder";
5 | import { root, main } from "./app.css";
6 |
7 | export default class App extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/spa/client/components/formFeedback/formFeedback.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { asideFontSize, asideLineHeight } from "../../styles/typoSizes";
3 | import { red } from "../../styles/colors";
4 | import latoLight from "../../styles/type/latoLight";
5 | import { msToSeconds } from "../../styles/timing";
6 |
7 | export const transitionDuration = 100;
8 |
9 | const transitionDurationCss = msToSeconds(100) + "s";
10 |
11 | export const overflowContainer = css({
12 | display: "inline-block",
13 | overflow: "hidden",
14 | transition: `height ${ transitionDurationCss } ease-in-out`,
15 | });
16 |
17 | export const message = css({
18 | ...latoLight,
19 | display: "inline-block",
20 | color: red(),
21 | fontSize: asideFontSize + "rem",
22 | lineHeight: asideLineHeight + "rem",
23 | minHeight: asideLineHeight + "rem",
24 | transform: "translateY(0)",
25 | transition: `transform ${ transitionDurationCss } ease-in-out`,
26 | ":empty": {
27 | transform: "translateY(-100%)",
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/spa/client/components/formFeedback/formFeedback.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import { overflowContainer, message } from "./formFeedback.css";
3 |
4 | export default class FormFeedback extends Component {
5 | render(props) {
6 | return (
7 |
8 |
9 | {props.children}
10 |
11 |
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/spa/client/components/header/common.js:
--------------------------------------------------------------------------------
1 | import { rem } from "../../styles/scales";
2 |
3 | // The type looks more vertically centered with this offset
4 | export const verticalOffset = 1;
5 | export const logoHeight = rem(17);
6 | export const headerCollapseBreakpoint = "@media (max-width: 35rem)";
7 |
--------------------------------------------------------------------------------
/spa/client/components/header/header.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { white, black } from "../../styles/colors";
3 | import { px, rem } from "../../styles/scales";
4 | import nexaHeavy from "../../styles/type/nexaHeavy";
5 | import { maxContentWidth } from "../../styles/layout";
6 | import { offscreen as a11yOffscreen } from "../../styles/a11y";
7 | import { header as headerZIndex } from "../../styles/zIndex";
8 | import { verticalOffset, logoHeight, headerCollapseBreakpoint } from "./common";
9 |
10 | export const root = css({
11 | position: "sticky",
12 | top: 0,
13 | zIndex: headerZIndex,
14 | color: black(),
15 | backgroundColor: white(),
16 | boxShadow: "0 5px 5px rgba(0, 0, 0, 0.1)",
17 | });
18 |
19 | export const content = css({
20 | display: "flex",
21 | alignItems: "center",
22 | lineHeight: logoHeight + "rem",
23 | flexWrap: "wrap",
24 | padding: [px(6) + verticalOffset, "px ", px(6), "px ", px(6) - verticalOffset, "px"].join(""),
25 | maxWidth: maxContentWidth + "rem",
26 | marginLeft: "auto",
27 | marginRight: "auto",
28 | });
29 |
30 | export const logo = css({
31 | display: "flex",
32 | alignItems: "center",
33 | textDecoration: "none",
34 | color: "currentColor",
35 | });
36 |
37 | export const nav = css({
38 | marginLeft: rem(12) + "rem",
39 | [headerCollapseBreakpoint]: {
40 | marginLeft: 0,
41 | width: "100%",
42 | order: 1,
43 | },
44 | });
45 |
46 | export const session = css({
47 | marginLeft: "auto",
48 | });
49 |
50 | export const headline = css({
51 | ...nexaHeavy,
52 | fontSize: rem(13) + "rem",
53 | margin: 0,
54 | marginLeft: px(10),
55 | });
56 |
57 | export const offscreen = css(a11yOffscreen);
58 |
--------------------------------------------------------------------------------
/spa/client/components/header/header.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Logo from "./logo/logo";
3 | import Nav from "./nav/nav";
4 | import Link from "../router/link";
5 | import routes from "../../routes";
6 | import { root, content, logo, nav, headline, session, offscreen } from "./header.css";
7 | import Session from "./session/session";
8 |
9 | export default class Header extends Component {
10 | render() {
11 | return (
12 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/spa/client/components/header/link.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px, rem } from "../../styles/scales";
3 | import nexaXBold from "../../styles/type/nexaXBold";
4 | import { regular as regularBorder } from "../../styles/borders";
5 |
6 | const activeLinkStyles = {
7 | borderTop: regularBorder("transparent"),
8 | borderBottom: regularBorder(),
9 | };
10 |
11 | export const activeLink = css(activeLinkStyles);
12 |
13 | export const link = css({
14 | ...nexaXBold,
15 | color: "currentColor",
16 | fontSize: rem(12) + "rem",
17 | textDecoration: "none",
18 | padding: `2px ${ px(5) }px`,
19 | ":hover": activeLinkStyles,
20 | ":active": activeLinkStyles,
21 | });
22 |
--------------------------------------------------------------------------------
/spa/client/components/header/logo/logo.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { verticalOffset, logoHeight } from "../common";
3 |
4 | export const logoImg = css({
5 | position: "relative",
6 | display: "block",
7 | height: logoHeight + "rem",
8 | top: -verticalOffset,
9 | });
10 |
--------------------------------------------------------------------------------
/spa/client/components/header/logo/logo.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import logoSrc from "../../../assets/img/peerigonLogoMint.svg";
3 | import { logoImg } from "./logo.css";
4 |
5 | export default class Logo extends Component {
6 | render() {
7 | return (
8 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/spa/client/components/header/nav/nav.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px } from "../../../styles/scales";
3 |
4 | export const list = css({
5 | display: "flex",
6 | listStyleType: "none",
7 | });
8 |
9 | export const listItem = css({
10 | ":not(:last-child)": {
11 | marginRight: px(10),
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/spa/client/components/header/nav/nav.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Link from "../../router/link";
3 | import { list, listItem } from "./nav.css";
4 | import { link, activeLink } from "../link.css";
5 | import routes from "../../../routes";
6 | import { nbsp } from "../../../util/htmlEntities";
7 |
8 | export default class Nav extends Component {
9 | render(props) {
10 | return (
11 |
12 |
13 |
14 |
15 | Top{nbsp}5
16 |
17 |
18 |
19 |
20 | All
21 |
22 |
23 |
24 |
25 | About
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/spa/client/components/header/session/anonymous/anonymous.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import URLSearchParams from "url-search-params";
3 | import Link from "../../../router/link";
4 | import Modal from "../../../modal/modal";
5 | import Placeholder from "../../../util/placeholder";
6 | import { link } from "../../link.css";
7 | import useDefault from "../../../../util/useDefault";
8 | import RoutingContext from "../../../router/util/routingContext";
9 | import { nbsp } from "../../../../util/htmlEntities";
10 |
11 | function loadLoginForm() {
12 | return useDefault(import("../../../loginForm/loginForm"));
13 | }
14 |
15 | export default class Anonymous extends Component {
16 | constructor() {
17 | super();
18 | this.loginFormProps = {
19 | handleLogin: this.handleLogin.bind(this),
20 | autoFocus: true,
21 | };
22 | this.routingContext = new RoutingContext(this);
23 | }
24 | handleLogin() {
25 | const current = this.routingContext.current();
26 | const paramsWithoutLogin = new URLSearchParams(window.location.search);
27 |
28 | paramsWithoutLogin.delete("showLogin");
29 |
30 | this.routingContext.next(current.route, paramsWithoutLogin);
31 | }
32 | render() {
33 | const paramsAndShowLogin = new URLSearchParams(window.location.search);
34 |
35 | paramsAndShowLogin.set("showLogin", 1);
36 |
37 | return (
38 |
39 |
40 | Log{nbsp}in
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/spa/client/components/header/session/personal/personal.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px } from "../../../../styles/scales";
3 | import latoLight from "../../../../styles/type/latoLight";
4 | import { headerCollapseBreakpoint, logoHeight, verticalOffset } from "../../common";
5 | import { regularFontSize } from "../../../../styles/typoSizes";
6 |
7 | export const root = css({
8 | display: "flex",
9 | alignItems: "center",
10 | fontSize: regularFontSize + "rem",
11 | // Avoids jumping header when the session state has changed
12 | lineHeight: 0,
13 | "> *:not(:last-child)": {
14 | marginRight: px(10),
15 | },
16 | });
17 |
18 | export const userName = css({
19 | ...latoLight,
20 | position: "relative",
21 | top: -verticalOffset,
22 | [headerCollapseBreakpoint]: {
23 | display: "none",
24 | },
25 | });
26 |
27 | export const userImage = css({
28 | position: "relative",
29 | top: -verticalOffset,
30 | display: "block",
31 | height: logoHeight + "rem",
32 | borderRadius: "100%",
33 | });
34 |
--------------------------------------------------------------------------------
/spa/client/components/header/session/personal/personal.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import destroySession from "../../../../api/session/destroy";
3 | import { root, userName, userImage } from "./personal.css";
4 | import { nbsp } from "../../../../util/htmlEntities";
5 | import { link } from "../../link.css";
6 |
7 | export default class Personal extends Component {
8 | constructor() {
9 | super();
10 | this.handleLogout = this.handleLogout.bind(this);
11 | }
12 | handleLogout() {
13 | destroySession();
14 | }
15 | render(props) {
16 | const user = props.user;
17 |
18 | if (user === null) {
19 | return null;
20 | }
21 |
22 | return (
23 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/spa/client/components/header/session/session.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Personal from "./personal/personal";
3 | import Anonymous from "./anonymous/anonymous";
4 | import localSession from "../../../api/session/local";
5 |
6 | export default class Profile extends Component {
7 | render(props) {
8 | const user = localSession.user;
9 |
10 | return (
11 |
12 | {user === null ?
:
}
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/spa/client/components/loading/loading.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import latoLight from "../../styles/type/latoLight";
3 | import { rem } from "../../styles/scales";
4 |
5 | export const root = css({
6 | ...latoLight,
7 | fontSize: rem(13) + "rem",
8 | display: "flex",
9 | width: "100%",
10 | height: "100%",
11 | minHeight: rem(20) + "rem",
12 | alignItems: "center",
13 | justifyContent: "center",
14 | });
15 |
--------------------------------------------------------------------------------
/spa/client/components/loading/loading.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import { root } from "./loading.css";
3 |
4 | export const loading = Loading...
;
5 |
6 | export default class Loading extends Component {
7 | render() {
8 | return loading;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/spa/client/components/loginForm/loginForm.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import sheet from "../../styles/block/sheet";
4 | import { regularFontSize, regularLineHeight } from "../../styles/typoSizes";
5 | import latoLight from "../../styles/type/latoLight";
6 | import inputText from "../../styles/block/inputText";
7 | import inputSubmit from "../../styles/block/inputSubmit";
8 | import { paddingBigger } from "../../styles/paddings";
9 |
10 | export const loginSheet = css({
11 | ...sheet,
12 | ...latoLight,
13 | maxWidth: rem(29) + "rem",
14 | boxSizing: "border-box",
15 | padding: paddingBigger,
16 | fontSize: regularFontSize + "rem",
17 | lineHeight: regularLineHeight + "rem",
18 | display: "flex",
19 | flexDirection: "column",
20 | });
21 |
22 | export const loginLabel = css({
23 | ":after": {
24 | content: JSON.stringify(":"),
25 | },
26 | });
27 |
28 | export const loginInput = css({
29 | ...inputText,
30 | });
31 |
32 | export const formFeedback = css({
33 | marginBottom: rem(10) + "rem",
34 | });
35 |
36 | export const loginSubmit = css(inputSubmit);
37 |
--------------------------------------------------------------------------------
/spa/client/components/loginForm/loginForm.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import FormFeedback from "../formFeedback/formFeedback";
3 | import loginFormValidators from "./loginFormValidators";
4 | import createSession from "../../api/session/create";
5 | import { loginSheet, loginInput, loginLabel, loginSubmit, formFeedback } from "./loginForm.css";
6 | import Form from "../form/form";
7 |
8 | export default class LoginForm extends Component {
9 | constructor() {
10 | super();
11 | this.renderForm = this.renderForm.bind(this);
12 | }
13 | renderForm({ id, errors, submitPending, submitError }) {
14 | const nameId = `${ id }-login-name`;
15 | const passwordId = `${ id }-login-password`;
16 | const { autoFocus = false } = this.props;
17 |
18 | /* eslint-disable react/jsx-key */
19 | return [
20 |
21 | Name
22 | ,
23 | ,
35 |
36 | {errors.get("name")}
37 | ,
38 |
39 | Password
40 | ,
41 | ,
51 |
52 | {errors.get("password")}
53 | {submitError === null ? null : submitError.message}
54 | ,
55 | ,
62 | ];
63 | /* eslint-enable react/jsx-key */
64 | }
65 | render(props, state) {
66 | return (
67 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/spa/client/components/loginForm/loginFormValidators.js:
--------------------------------------------------------------------------------
1 | const validators = new Map();
2 |
3 | validators.set("name", values => {
4 | const name = values.get("name");
5 |
6 | if (name === "") {
7 | return "Missing login name";
8 | }
9 |
10 | return null;
11 | });
12 | validators.set("password", values => {
13 | const password = values.get("password");
14 |
15 | if (password === "") {
16 | return "Missing password";
17 | }
18 |
19 | return null;
20 | });
21 |
22 | export default validators;
23 |
--------------------------------------------------------------------------------
/spa/client/components/modal/modal.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import hexToRgba from "hex-to-rgba";
3 | import { backdrop as backdropZIndex, modal as modalZIndex } from "../../styles/zIndex";
4 | import { msToSeconds } from "../../styles/timing";
5 | import { white, mint } from "../../styles/colors";
6 | import { paddingRegular } from "../../styles/paddings";
7 | import calc from "../../styles/calc";
8 |
9 | export const fadeDuration = 100;
10 | const fadeDurationCss = msToSeconds(fadeDuration) + "s";
11 | const backdropOpacity = 0.4;
12 |
13 | const backdropGlowAnimation = css.keyframes({
14 | "50%": {
15 | backgroundColor: hexToRgba(mint(), "0.2"),
16 | },
17 | });
18 |
19 | export const root = css({
20 | position: "absolute",
21 | top: 0,
22 | width: "100vw",
23 | height: "100vh",
24 | });
25 |
26 | export const backdrop = css({
27 | position: "fixed",
28 | top: 0,
29 | left: 0,
30 | right: 0,
31 | bottom: 0,
32 | zIndex: backdropZIndex,
33 | backgroundColor: "black",
34 | ":focus": {
35 | animation: `${ backdropGlowAnimation } infinite 3s ease-in-out`,
36 | },
37 | });
38 |
39 | export const backdropHidden = css({
40 | opacity: 0,
41 | transition: `opacity ${ fadeDurationCss } ease-in-out, transform 0s ${ fadeDurationCss }`,
42 | transform: "translateX(100%)",
43 | });
44 |
45 | export const backdropVisible = css({
46 | opacity: backdropOpacity,
47 | transition: `opacity ${ fadeDurationCss } ease-in-out`,
48 | });
49 |
50 | export const window = css({
51 | position: "fixed",
52 | zIndex: modalZIndex,
53 | top: "50%",
54 | left: "50%",
55 | transform: "translate(-50%, -50%)",
56 | backgroundColor: white(),
57 | boxShadow: "0 7px 7px rgba(0, 0, 0, 0.3)",
58 | "> *": {
59 | width: calc("100vw - ", paddingRegular * 2, "px"),
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/spa/client/components/modal/modal.js:
--------------------------------------------------------------------------------
1 | import { Component, render as preactRender } from "preact";
2 | import URLSearchParams from "url-search-params";
3 | import WithContext from "../util/withContext";
4 | import { root, backdrop, backdropHidden, backdropVisible, fadeDuration, window as modalWindow } from "./modal.css";
5 | import GoBack from "../router/goBack";
6 |
7 | function getBackParams(modalParam) {
8 | const params = new URLSearchParams(window.location.search);
9 |
10 | params.delete(modalParam);
11 |
12 | return params;
13 | }
14 |
15 | export default class Modal extends Component {
16 | constructor(props) {
17 | super();
18 | this.setState({ active: false });
19 | this.renderContainer = document.createElement("section");
20 | this.updateActiveState(props);
21 | }
22 | componentWillReceiveProps(newProps) {
23 | this.updateActiveState(newProps);
24 | }
25 | componentWillUnmount() {
26 | // Trigger renderModal manually
27 | this.updateActiveState(this.props);
28 | this.renderModal(this.props, this.state);
29 | }
30 | updateActiveState(props) {
31 | this.setState(prevState => {
32 | const active = new URLSearchParams(location.search).has(props.activationParam) === true;
33 |
34 | if (prevState.active === true && active === false) {
35 | setTimeout(() => {
36 | if (this.state.active === true) {
37 | return;
38 | }
39 | document.body.removeChild(this.renderContainer);
40 | }, fadeDuration);
41 | } else if (prevState.active === false && active === true) {
42 | document.body.appendChild(this.renderContainer);
43 | // Trigger reflow to kick off the transition
44 | void this.renderContainer.offsetHeight;
45 | }
46 |
47 | return {
48 | active,
49 | };
50 | });
51 | }
52 | renderModal(props, state) {
53 | const backdropClass = [backdrop, state.active === true ? backdropVisible : backdropHidden];
54 |
55 | preactRender(
56 |
57 |
58 |
59 | {props.render || state.active ?
60 |
61 | {props.children}
62 |
:
63 | null}
64 |
65 | ,
66 | this.renderContainer,
67 | this.renderContainer.firstElementChild
68 | );
69 | }
70 | render(props, state) {
71 | this.renderModal(props, state);
72 |
73 | return null;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/spa/client/components/notFound/notFound.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Header from "../header/header";
3 |
4 | export default class NotFound extends Component {
5 | render() {
6 | return (
7 |
8 |
9 |
Not Found
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/spa/client/components/posts/post/post.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import nexaHeavy from "../../../styles/type/nexaHeavy";
3 | import latoLight from "../../../styles/type/latoLight";
4 | import { rem } from "../../../styles/scales";
5 | import {
6 | regularFontSize,
7 | regularLineHeight,
8 | regularMaxWidth,
9 | headlineFontSize,
10 | headlineLineHeight,
11 | headlineMaxWidth,
12 | } from "../../../styles/typoSizes";
13 |
14 | export const headline = css({
15 | ...nexaHeavy,
16 | fontSize: headlineFontSize + "rem",
17 | lineHeight: headlineLineHeight + "rem",
18 | maxWidth: headlineMaxWidth + "rem",
19 | marginBottom: rem(6) + "rem",
20 | });
21 |
22 | export const aside = css({
23 | ...latoLight,
24 | display: "block",
25 | fontSize: rem(11) + "rem",
26 | lineHeight: rem(12) + "rem",
27 | marginBottom: rem(13) + "rem",
28 | });
29 |
30 | export const paragraph = css({
31 | ...latoLight,
32 | fontSize: regularFontSize + "rem",
33 | lineHeight: regularLineHeight + "rem",
34 | maxWidth: regularMaxWidth + "rem",
35 | ":not(:last-child)": {
36 | marginBottom: rem(10) + "rem",
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/spa/client/components/posts/post/post.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import fromNow from "from-now";
3 | import { headline, paragraph, aside } from "./post.css";
4 |
5 | const lineBreak = /\s*[\r\n]+\s*/g;
6 |
7 | export default class Post extends Component {
8 | render(props, state) {
9 | const post = props.post;
10 |
11 | return (
12 |
13 |
14 | {post.title}
15 |
16 |
17 |
18 | {fromNow(post.published)}
19 |
20 | {" ago by "}
21 | {post.author}
22 |
23 |
24 | {post.content.split(lineBreak).map(p =>
25 | (
26 | {p}
27 |
)
28 | )}
29 |
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/spa/client/components/posts/posts.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px, rem } from "../../styles/scales";
3 | import { offscreen } from "../../styles/a11y";
4 | import { regularMaxWidth } from "../../styles/typoSizes";
5 | import sheet, { sheetPadding } from "../../styles/block/sheet";
6 |
7 | export const root = css({
8 | position: "relative",
9 | });
10 |
11 | export const a11yTitle = css({
12 | ...offscreen,
13 | });
14 |
15 | export const postImage = css({
16 | position: "absolute",
17 | maxWidth: px(30),
18 | marginTop: sheetPadding,
19 | transform: "translate(0%)",
20 | transition: "transform 0.3s ease-in-out",
21 | });
22 |
23 | export const postSheet = css({
24 | ...sheet,
25 | // position relative is necessary so that the position absolute image is still below the sheet
26 | position: "relative",
27 | maxWidth: regularMaxWidth + "rem",
28 | });
29 |
30 | export const postContainer = css({
31 | position: "relative",
32 | overflow: "hidden",
33 | ":not(:last-child)": {
34 | marginBottom: rem(15) + "rem",
35 | },
36 | [":nth-child(odd) ." + postSheet]: {
37 | marginLeft: "auto",
38 | },
39 | [":nth-child(odd) ." + postImage]: {
40 | left: px(17),
41 | },
42 | [":nth-child(even) ." + postImage]: {
43 | right: px(17),
44 | },
45 | [":nth-child(odd):not(:hover) ." + postImage]: {
46 | transform: "translate(10%)",
47 | },
48 | [":nth-child(even):not(:hover) ." + postImage]: {
49 | transform: "translate(-10%)",
50 | },
51 | });
52 |
--------------------------------------------------------------------------------
/spa/client/components/posts/posts.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import AsyncPropsCache from "../../util/asyncPropsCache";
3 | import Post from "./post/post";
4 | import { a11yTitle, root, postContainer, postSheet, postImage } from "./posts.css";
5 |
6 | const empty = [];
7 |
8 | export default class Posts extends Component {
9 | constructor(props) {
10 | super();
11 | this.asyncPropsCache = new AsyncPropsCache(this, {
12 | posts: props.posts,
13 | });
14 | }
15 | render(props, state) {
16 | const posts = state.posts;
17 |
18 | return (
19 |
20 |
21 | {props.a11yTitle}
22 |
23 | {posts === null ?
24 | empty :
25 | posts.map(post =>
26 | (
27 |
28 |
29 |
)
30 | )}
31 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/spa/client/components/router/goBack.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Link from "./link";
3 |
4 | export default class GoBack extends Component {
5 | render(props) {
6 | return (
7 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/spa/client/components/router/link.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import createEventHandler from "../../util/createEventHandler";
3 | import routeToHref from "./util/routeToHref";
4 |
5 | export default class Link extends Component {
6 | constructor() {
7 | super();
8 |
9 | const preloadNextComponent = this.preloadNextComponent.bind(this);
10 |
11 | this.handleMouseOver = createEventHandler(this, "onMouseOver", preloadNextComponent);
12 | this.handleFocus = createEventHandler(this, "onFocus", preloadNextComponent);
13 | }
14 | preloadNextComponent() {
15 | const route = this.props.route;
16 | const component = route && route.component;
17 |
18 | if (typeof component === "function") {
19 | component();
20 | }
21 | }
22 | splitProps(props) {
23 | const aProps = (this.aProps = {});
24 | const ownProps = (this.ownProps = {
25 | route: props.route || this.context.route,
26 | params: props.params || null,
27 | children: props.children,
28 | replaceRoute: Boolean(props.replaceRoute),
29 | activeClass: props.activeClass || "",
30 | });
31 |
32 | Object.keys(props).filter(key => key in ownProps === false).forEach(key => {
33 | aProps[key] = props[key];
34 | });
35 | }
36 | render() {
37 | this.splitProps(this.props);
38 |
39 | const { route, params, children, replaceRoute, activeClass } = this.ownProps;
40 | const classes = [route === this.context.route ? activeClass : "", this.aProps.class];
41 |
42 | return (
43 |
52 | {children}
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/spa/client/components/router/routePlaceholder.js:
--------------------------------------------------------------------------------
1 | import Placeholder from "../util/placeholder";
2 |
3 | const defaultParams = {};
4 |
5 | export default class RoutePlaceholder {
6 | render(props, state) {
7 | const route = this.context.route;
8 |
9 | return ;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/spa/client/components/router/router.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import URLSearchParams from "url-search-params";
3 | import nanorouter from "nanorouter";
4 | import onLinkClick from "nanohref";
5 | import onHistoryPop from "nanohistory";
6 | import routes from "../../routes";
7 | import trigger from "./util/trigger";
8 |
9 | function createRouteHandler(route, component) {
10 | return urlParams => {
11 | const params = new URLSearchParams(window.location.search);
12 |
13 | for (const key of Object.keys(urlParams)) {
14 | params.set(key, urlParams[key]);
15 | }
16 |
17 | component.setState({
18 | previousRoute: component.state.route || null,
19 | previousParams: component.state.params || null,
20 | route,
21 | params,
22 | });
23 | };
24 | }
25 |
26 | function createRouter(routes, component) {
27 | const router = nanorouter({ default: "/404" });
28 |
29 | Object.values(routes).forEach(route => {
30 | router.on(route.match, createRouteHandler(route, component));
31 | });
32 |
33 | onHistoryPop(location => {
34 | router(location.pathname);
35 | });
36 | onLinkClick(node => {
37 | const href = node.href;
38 |
39 | if (node.hasAttribute("data-route") === false) {
40 | window.location = href;
41 |
42 | return;
43 | }
44 | trigger(router, node.href, {
45 | replaceRoute: node.hasAttribute("data-replace-url") ? true : undefined,
46 | });
47 | });
48 |
49 | return router;
50 | }
51 |
52 | export default class Router extends Component {
53 | constructor() {
54 | super();
55 | this.router = createRouter(routes, this);
56 | this.router(location.pathname);
57 | }
58 | getChildContext() {
59 | return {
60 | route: this.state.route || null,
61 | params: this.state.params || null,
62 | previousRoute: this.state.previousRoute || null,
63 | previousParams: this.state.previousParams || null,
64 | router: this.router,
65 | };
66 | }
67 | componentWillUnmount() {
68 | // We cannot undo the side-effects introduced by the router
69 | throw new Error("Cannot unmount router: The router is intended to be used top-level");
70 | }
71 | render(props, state) {
72 | return props.children[0];
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/spa/client/components/router/util/routeToHref.js:
--------------------------------------------------------------------------------
1 | export default function routeToHref(route, params) {
2 | if (params === null) {
3 | return route.match;
4 | }
5 |
6 | let href = route.match;
7 |
8 | for (const key of params.keys()) {
9 | const pattern = ":" + key;
10 | const patternIdx = href.indexOf(pattern);
11 |
12 | if (patternIdx > -1) {
13 | params.delete(key);
14 |
15 | href = href.slice(0, patternIdx - 1) + params.get(key) + href.slice(patternIdx + pattern.length);
16 | }
17 | }
18 |
19 | const paramString = params.toString();
20 |
21 | if (paramString === "") {
22 | return href;
23 | }
24 |
25 | return href + "?" + paramString;
26 | }
27 |
--------------------------------------------------------------------------------
/spa/client/components/router/util/routingContext.js:
--------------------------------------------------------------------------------
1 | import trigger from "./trigger";
2 | import routeToHref from "./routeToHref";
3 |
4 | export default class RoutingContext {
5 | constructor(component) {
6 | this.component = component;
7 | }
8 | previous() {
9 | return {
10 | route: this.component.context.previousRoute,
11 | params: this.component.context.previousParams,
12 | };
13 | }
14 | current() {
15 | return {
16 | route: this.component.context.route,
17 | params: this.component.context.params,
18 | };
19 | }
20 | next(route, params = null, options) {
21 | trigger(this.component.context.router, routeToHref(route, params), options);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/spa/client/components/router/util/trigger.js:
--------------------------------------------------------------------------------
1 | export default function trigger(router, href, { replaceRoute = href === window.location.href } = {}) {
2 | const saveState = replaceRoute === true ? window.history.replaceState : window.history.pushState;
3 |
4 | saveState.call(history, {}, "", href);
5 | router(location.pathname);
6 | }
7 |
--------------------------------------------------------------------------------
/spa/client/components/top5/top5.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import getTop5 from "../../api/posts/getTop5";
3 | import Posts from "../posts/posts";
4 | import WithTitle from "../util/withTitle";
5 |
6 | export default class Top5 extends Component {
7 | render() {
8 | const title = "Top 5 Peerigon News";
9 |
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/spa/client/components/util/placeholder.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import Loading from "../loading/loading";
3 | import AsyncPropsCache from "../../util/asyncPropsCache";
4 |
5 | export default class Placeholder extends Component {
6 | constructor(props) {
7 | super();
8 | this.asyncPropsCache = new AsyncPropsCache(this, {
9 | component: props.component,
10 | });
11 | }
12 | render(props, state) {
13 | const Component = state.component;
14 |
15 | if (Component !== null) {
16 | return ;
17 | }
18 |
19 | const children = props.children;
20 |
21 | if (children.length === 0) {
22 | return ;
23 | }
24 |
25 | const childGenerator = children[0];
26 |
27 | return childGenerator(state.componentError);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/spa/client/components/util/withContext.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 |
3 | export default class WithContext extends Component {
4 | getChildContext() {
5 | return this.props.context;
6 | }
7 | render(props) {
8 | return props.children[0];
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/spa/client/components/util/withTitle.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 |
3 | export default class WithTitle extends Component {
4 | componentWillReceiveProps(props) {
5 | this.updateTitle(props.title);
6 | }
7 | componentWillMount() {
8 | this.updateTitle(this.props.title);
9 | }
10 | updateTitle(title) {
11 | if (title !== undefined) {
12 | document.title = title;
13 | }
14 | }
15 | render(props) {
16 | return props.children[0];
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/spa/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Peerigon News
11 |
12 |
13 |
14 |
15 | You need to enable JavaScript to run this app.
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/spa/client/index.js:
--------------------------------------------------------------------------------
1 | require("./init/globals");
2 | require("./init/styles");
3 | require("./init/serviceWorker");
4 | require("./init/render");
5 |
--------------------------------------------------------------------------------
/spa/client/init/globals.js:
--------------------------------------------------------------------------------
1 | // A place for global variables and polyfills
2 | import { h } from "preact";
3 |
4 | window.h = h;
5 |
--------------------------------------------------------------------------------
/spa/client/init/render.js:
--------------------------------------------------------------------------------
1 | import { render as preactRender } from "preact";
2 | import App from "../components/app/app";
3 |
4 | function render() {
5 | preactRender( , document.body);
6 | }
7 |
8 | document.addEventListener("DOMContentLoaded", render);
9 |
--------------------------------------------------------------------------------
/spa/client/init/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // TODO
2 | export default function register() {
3 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
4 | window.addEventListener("load", () => {
5 | navigator.serviceWorker
6 | // Provided by the sw-precache-webpack-plugin
7 | .register("/service-worker.js")
8 | .then(registration => {
9 | registration.onupdatefound = () => {
10 | const installingWorker = registration.installing;
11 |
12 | installingWorker.onstatechange = () => {
13 | if (installingWorker.state === "installed") {
14 | if (navigator.serviceWorker.controller) {
15 | console.log("New content is available; please refresh.");
16 | } else {
17 | console.log("Content is cached for offline use.");
18 | }
19 | }
20 | };
21 | };
22 | })
23 | .catch(error => {
24 | console.error("Error during service worker registration:", error);
25 | });
26 | });
27 | }
28 | }
29 |
30 | export function unregister() {
31 | if ("serviceWorker" in navigator) {
32 | navigator.serviceWorker.ready.then(registration => {
33 | registration.unregister();
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/spa/client/init/styles.js:
--------------------------------------------------------------------------------
1 | // Pre-styles will be extracted and inlined into the index.html
2 | import "../styles/pre/reset.css";
3 | import "../styles/pre/body.css";
4 |
--------------------------------------------------------------------------------
/spa/client/routes.js:
--------------------------------------------------------------------------------
1 | import useDefault from "./util/useDefault";
2 |
3 | export default {
4 | top5: {
5 | match: "/",
6 | component: () => useDefault(import("./components/top5/top5" /* webpackChunkName: "posts" */)),
7 | },
8 | allPosts: {
9 | match: "/all",
10 | component: () => useDefault(import("./components/allPosts/allPosts" /* webpackChunkName: "posts" */)),
11 | },
12 | about: {
13 | match: "/about",
14 | component: () => useDefault(import("./components/about/about" /* webpackChunkName: "about" */)),
15 | },
16 | notFound: {
17 | match: "/404",
18 | component: () => useDefault(import("./components/notFound/notFound" /* webpackChunkName: "notFound" */)),
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/spa/client/styles/a11y.js:
--------------------------------------------------------------------------------
1 | export const offscreen = {
2 | position: "absolute",
3 | left: -10000,
4 | top: "auto",
5 | width: 1,
6 | height: 1,
7 | overflow: "hidden",
8 | };
9 |
--------------------------------------------------------------------------------
/spa/client/styles/block/inputSubmit.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import { mint } from "../../styles/colors";
4 | import { regularFontSize, regularLineHeight } from "../../styles/typoSizes";
5 | import nexaXBold from "../../styles/type/nexaXBold";
6 | import { regular } from "../borders";
7 | import { repeatingLinear } from "../gradients";
8 |
9 | const stripeColor = "rgba(0, 0, 0, 0.1)";
10 | const stripeAnimation = css.keyframes({
11 | from: {
12 | backgroundPosition: "0 0",
13 | },
14 | to: {
15 | backgroundPosition: "71px 0px",
16 | },
17 | });
18 | // Mobile safari adds weird styles
19 | const mobileSafariStyleFixes = {
20 | WebkitAppearance: "none",
21 | borderRadius: 0,
22 | };
23 |
24 | export default {
25 | ...nexaXBold,
26 | ...mobileSafariStyleFixes,
27 | width: "100%",
28 | fontSize: regularFontSize + "rem",
29 | lineHeight: regularLineHeight + "rem",
30 | padding: rem(7) + "rem 0",
31 | border: "none",
32 | outline: "none",
33 | backgroundColor: mint(),
34 | boxShadow: "0 0px 0px rgba(0, 0, 0, 0.3)",
35 | transition: "box-shadow 0.1s ease-in-out",
36 | ":hover": {
37 | cursor: "pointer",
38 | boxShadow: "3px 3px 3px rgba(0, 0, 0, 0.2), -3px 3px 3px rgba(0, 0, 0, 0.2)",
39 | },
40 | ":focus": {
41 | outline: regular(),
42 | },
43 | "[data-pending]": {
44 | backgroundImage: repeatingLinear("-45deg", [
45 | "transparent 0",
46 | "transparent 25px",
47 | stripeColor + " 25px",
48 | stripeColor + " 50px",
49 | ]),
50 | backgroundSize: "71px 50px",
51 | animation: stripeAnimation + " 2s linear infinite",
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/spa/client/styles/block/inputText.js:
--------------------------------------------------------------------------------
1 | import { rem } from "../../styles/scales";
2 | import { mint, red } from "../../styles/colors";
3 | import { regularFontSize, regularLineHeight } from "../../styles/typoSizes";
4 | import latoLight from "../../styles/type/latoLight";
5 | import { regular } from "../borders";
6 |
7 | const mobileSafariStyleFixes = {
8 | WebkitAppearance: "none",
9 | borderRadius: 0,
10 | };
11 |
12 | export default {
13 | ...latoLight,
14 | ...mobileSafariStyleFixes,
15 | width: "100%",
16 | fontSize: regularFontSize + "rem",
17 | lineHeight: regularLineHeight + "rem",
18 | padding: rem(7) + "rem 0",
19 | border: "none",
20 | borderBottom: regular(),
21 | outline: "none",
22 | ":focus": {
23 | borderColor: mint(),
24 | outlineColor: mint(),
25 | },
26 | "[invalid]": {
27 | borderBottomColor: red(),
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/spa/client/styles/block/sheet.js:
--------------------------------------------------------------------------------
1 | import { px } from "../../styles/scales";
2 | import { white, black } from "../../styles/colors";
3 |
4 | export const sheetPadding = px(13);
5 |
6 | export default {
7 | color: black(),
8 | backgroundColor: white(),
9 | padding: sheetPadding,
10 | };
11 |
--------------------------------------------------------------------------------
/spa/client/styles/borders.js:
--------------------------------------------------------------------------------
1 | import { black } from "./colors";
2 |
3 | export const defaultColor = black();
4 | export const regularWidth = 2;
5 | export const strongWidth = 4;
6 |
7 | export function regular(color = defaultColor) {
8 | return `${ regularWidth }px solid ${ color }`;
9 | }
10 |
11 | export function strong(color = defaultColor) {
12 | return `${ strongWidth }px solid ${ color }`;
13 | }
14 |
--------------------------------------------------------------------------------
/spa/client/styles/calc.js:
--------------------------------------------------------------------------------
1 | export default function (...bits) {
2 | return `calc(${ bits.join("") })`;
3 | }
4 |
--------------------------------------------------------------------------------
/spa/client/styles/colors.js:
--------------------------------------------------------------------------------
1 | import { color, lightness } from "kewler";
2 |
3 | export const mint = color("#46e1c8");
4 | export const mintLight35 = mint(lightness(35));
5 | export const white = color("#fff");
6 | export const silver = color("#e6e1de");
7 | export const silverLight10 = silver(lightness(10));
8 | export const black = color("#282828");
9 | export const blackLight30 = black(lightness(40));
10 | export const red = color("#f14936");
11 |
--------------------------------------------------------------------------------
/spa/client/styles/gradients.js:
--------------------------------------------------------------------------------
1 | export function linear(dir, colorStops) {
2 | return `linear-gradient(${ dir }, ${ colorStops.join(", ") })`;
3 | }
4 |
5 | export function repeatingLinear(dir, colorStops) {
6 | return `repeating-linear-gradient(${ dir }, ${ colorStops.join(", ") })`;
7 | }
8 |
--------------------------------------------------------------------------------
/spa/client/styles/layout.js:
--------------------------------------------------------------------------------
1 | import { rem } from "./scales";
2 |
3 | export const maxContentWidth = rem(34);
4 |
--------------------------------------------------------------------------------
/spa/client/styles/paddings.js:
--------------------------------------------------------------------------------
1 | import { px } from "./scales";
2 |
3 | export const paddingRegular = px(14);
4 | export const paddingBigger = px(15);
5 |
--------------------------------------------------------------------------------
/spa/client/styles/pre/body.css:
--------------------------------------------------------------------------------
1 | body {
2 | position: relative;
3 | }
--------------------------------------------------------------------------------
/spa/client/styles/pre/reset.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
--------------------------------------------------------------------------------
/spa/client/styles/scales.js:
--------------------------------------------------------------------------------
1 | const pxBase = 2;
2 | const pxRatio = 6 / 5;
3 | const remBase = 2 / 16;
4 | const remRatio = 6 / 5;
5 | const regularDevicePixelRatio = 2;
6 |
7 | export function px(factor) {
8 | const size = Math.pow(pxRatio, factor) * pxBase;
9 |
10 | // Round to whole device pixels
11 | return Math.floor(size * regularDevicePixelRatio) / regularDevicePixelRatio;
12 | }
13 |
14 | export function rem(factor) {
15 | return Math.pow(remRatio, factor) * remBase;
16 | }
17 |
--------------------------------------------------------------------------------
/spa/client/styles/timing.js:
--------------------------------------------------------------------------------
1 | export function msToSeconds(ms) {
2 | return ms / 1000;
3 | }
4 |
--------------------------------------------------------------------------------
/spa/client/styles/type/fonts/latoLight.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Lato";
3 | src: local("Lato Light"),
4 | url("../../../assets/fonts/latoLatinLight.woff2") format("woff2"),
5 | url("../../../assets/fonts/latoLatinLight.woff") format("woff"),
6 | url("../../../assets/fonts/latoLatinLight.ttf") format("truetype");
7 | font-weight: 200;
8 | font-style: normal;
9 | }
--------------------------------------------------------------------------------
/spa/client/styles/type/fonts/nexaHeavy.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Nexa";
3 | src: local("Nexa Heavy"),
4 | url("../../../assets/fonts/nexaHeavyWebfont.woff2") format("woff2"),
5 | url("../../../assets/fonts/nexaHeavyWebfont.woff") format("woff"),
6 | url("../../../assets/fonts/nexaHeavyWebfont.ttf") format("truetype");
7 | font-weight: 900;
8 | font-style: normal;
9 | }
--------------------------------------------------------------------------------
/spa/client/styles/type/fonts/nexaXbold.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Nexa";
3 | src: local("Nexa XBold"),
4 | url("../../../assets/fonts/nexaExtraboldWebfont.woff2") format("woff2"),
5 | url("../../../assets/fonts/nexaExtraboldWebfont.woff") format("woff"),
6 | url("../../../assets/fonts/nexaExtraboldWebfont.ttf") format("truetype");
7 | font-weight: 700;
8 | font-style: normal;
9 | }
--------------------------------------------------------------------------------
/spa/client/styles/type/latoLight.js:
--------------------------------------------------------------------------------
1 | import "./fonts/latoLight.css"; // eslint-disable-line import/no-unassigned-import
2 |
3 | export default {
4 | fontFamily: "Lato",
5 | fontWeight: 200,
6 | fontStyle: "normal",
7 | };
8 |
--------------------------------------------------------------------------------
/spa/client/styles/type/nexaHeavy.js:
--------------------------------------------------------------------------------
1 | import "./fonts/nexaHeavy.css"; // eslint-disable-line import/no-unassigned-import
2 |
3 | export default {
4 | fontFamily: "Nexa",
5 | fontWeight: 900,
6 | fontStyle: "normal",
7 | };
8 |
--------------------------------------------------------------------------------
/spa/client/styles/type/nexaXBold.js:
--------------------------------------------------------------------------------
1 | import "./fonts/nexaXBold.css"; // eslint-disable-line import/no-unassigned-import
2 |
3 | export default {
4 | fontFamily: "Nexa",
5 | fontWeight: 700,
6 | fontStyle: "normal",
7 | };
8 |
--------------------------------------------------------------------------------
/spa/client/styles/typoSizes.js:
--------------------------------------------------------------------------------
1 | import { rem } from "./scales";
2 |
3 | export const regularFontSize = rem(12);
4 | export const regularLineHeight = rem(14);
5 | export const regularMaxWidth = rem(32);
6 |
7 | export const headlineFontSize = rem(14);
8 | export const headlineLineHeight = rem(15);
9 | export const headlineMaxWidth = rem(30);
10 |
11 | export const asideFontSize = rem(11);
12 | export const asideLineHeight = rem(12);
13 | export const asideMaxWidth = rem(30);
14 |
--------------------------------------------------------------------------------
/spa/client/styles/zIndex.js:
--------------------------------------------------------------------------------
1 | export const header = 2;
2 | export const modal = 3;
3 | export const backdrop = 1;
4 |
--------------------------------------------------------------------------------
/spa/client/util/asyncContext.js:
--------------------------------------------------------------------------------
1 | export default class AsyncContext {
2 | constructor(component) {
3 | const unmountHandler = component.componentWillUnmount;
4 |
5 | this.component = component;
6 | this.pending = new Map();
7 |
8 | component.componentWillUnmount = (...args) => {
9 | const result = unmountHandler ? unmountHandler.apply(component, args) : undefined;
10 |
11 | this.reset();
12 |
13 | return result;
14 | };
15 | }
16 | add(name, promise, initialValue) {
17 | const proceed = () => this.pending.get(name) === promise;
18 |
19 | this.setStartState(name, initialValue);
20 | this.pending.set(name, promise);
21 |
22 | return promise.then(
23 | res => {
24 | if (proceed() === true) {
25 | this.pending.delete("name");
26 | this.setSuccessState(name, res);
27 |
28 | return res;
29 | }
30 |
31 | return null;
32 | },
33 | err => {
34 | if (proceed() === true) {
35 | this.pending.delete("name");
36 | this.setFailState(name, err);
37 |
38 | throw err;
39 | }
40 |
41 | console.log("An error happened in an abandoned async context");
42 | console.log(err);
43 |
44 | return null;
45 | }
46 | );
47 | }
48 | setStartState(name, initialValue) {
49 | this.component.setState({
50 | [name + "Pending"]: true,
51 | [name + "Error"]: null,
52 | [name]: initialValue,
53 | });
54 | }
55 | setSuccessState(name, result) {
56 | this.component.setState({
57 | [name + "Pending"]: false,
58 | [name + "Error"]: null,
59 | [name]: result,
60 | });
61 | }
62 | setFailState(name, error) {
63 | this.component.setState({
64 | [name + "Pending"]: false,
65 | [name + "Error"]: error,
66 | [name]: null,
67 | });
68 | }
69 | reset() {
70 | this.pending.clear();
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/spa/client/util/asyncPropsCache.js:
--------------------------------------------------------------------------------
1 | import AsyncContext from "./asyncContext";
2 |
3 | export default class AsyncPropsCache {
4 | constructor(component, asyncProps) {
5 | const willReceiveProps = component.componentWillReceiveProps;
6 |
7 | this.component = component;
8 | this.asyncProps = asyncProps;
9 | this.async = new AsyncContext(component);
10 | this.cache = new Map();
11 |
12 | component.componentWillReceiveProps = (...args) => {
13 | const nextProps = args[0];
14 | const result = willReceiveProps ? willReceiveProps.apply(component, args) : undefined;
15 |
16 | this.fetchNewProps(nextProps);
17 |
18 | return result;
19 | };
20 |
21 | this.fetchNewProps(asyncProps);
22 | }
23 | fetchNewProps(props) {
24 | Object.keys(this.asyncProps)
25 | .filter(key => this.cache.has(key) === false || this.cache.get(key) !== props[key])
26 | .forEach(key => {
27 | const prop = props[key];
28 |
29 | this.async.add(key, prop(), null);
30 | this.cache.set(key, prop);
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/spa/client/util/createEventHandler.js:
--------------------------------------------------------------------------------
1 | export default function createEventHandler(component, eventProp, handler) {
2 | return e => {
3 | const originalEventHandler = component.props[eventProp];
4 |
5 | handler.call(component, e);
6 | if (typeof originalEventHandler === "function") {
7 | originalEventHandler.call(e.currentTarget, e);
8 | }
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/spa/client/util/generateId.js:
--------------------------------------------------------------------------------
1 | let counter = 0;
2 |
3 | export default function generateId() {
4 | return "app-id-" + counter++;
5 | }
6 |
--------------------------------------------------------------------------------
/spa/client/util/htmlEntities.js:
--------------------------------------------------------------------------------
1 | export const nbsp = "\u00a0";
2 |
--------------------------------------------------------------------------------
/spa/client/util/mapToObject.js:
--------------------------------------------------------------------------------
1 | export default function mapToObject(map) {
2 | const obj = Object.create(null);
3 |
4 | for (const [key, value] of map.entries()) {
5 | obj[key] = value;
6 | }
7 |
8 | return obj;
9 | }
10 |
--------------------------------------------------------------------------------
/spa/client/util/useDefault.js:
--------------------------------------------------------------------------------
1 | export default function useDefault(p) {
2 | return p.then(e => e.default);
3 | }
4 |
--------------------------------------------------------------------------------
/spa/config/server.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 3000,
3 | "hostname": "localhost",
4 | "bodyLimit": "100kb",
5 | "corsHeaders": [
6 | "Link"
7 | ],
8 | "responseDelay": 300
9 | }
--------------------------------------------------------------------------------
/spa/server/dummyData/generate.js:
--------------------------------------------------------------------------------
1 | import filledArray from "filled-array";
2 | import faker from "faker";
3 |
4 | function posts() {
5 | return filledArray(
6 | i => ({
7 | id: faker.random.uuid(),
8 | title: faker.lorem.sentence(),
9 | content: faker.lorem.paragraphs(),
10 | author: faker.name.findName(),
11 | published: faker.date.past(),
12 | starred: Math.floor(Math.random() * 100),
13 | image: `/postImage${ i % 4 + 1 }.jpg`,
14 | }),
15 | 30
16 | );
17 | }
18 |
19 | posts();
20 |
--------------------------------------------------------------------------------
/spa/server/dummyData/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "353e4bdf-7436-41c1-a25c-25f0667692d9",
4 | "name": "jhnns",
5 | "image": "userImage.jpg"
6 | }
7 | ]
--------------------------------------------------------------------------------
/spa/server/env.js:
--------------------------------------------------------------------------------
1 | const env = process.env.NODE_ENV || "development";
2 |
3 | export const isProd = env === "production";
4 | export const isDev = isProd === false;
5 |
6 | export default env;
7 |
--------------------------------------------------------------------------------
/spa/server/index.js:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import path from "path";
3 | import express from "express";
4 | import cors from "cors";
5 | import morgan from "morgan";
6 | import connectGzipStatic from "connect-gzip-static";
7 | import helmet from "helmet";
8 | import config from "../config/server";
9 | import api from "./api";
10 |
11 | const app = express();
12 | const pathToIndexHtml = path.resolve(__dirname, "..", "public", "index.html");
13 |
14 | app.server = http.createServer(app);
15 |
16 | app.use(morgan("dev"));
17 | app.use(helmet());
18 | app.use(
19 | cors({
20 | exposedHeaders: config.corsHeaders,
21 | })
22 | );
23 | api(app);
24 | app.use(
25 | connectGzipStatic(path.resolve(__dirname, "..", "public"), {
26 | // We use hashed filenames, a long max age is ok
27 | maxAge: 365 * 24 * 60 * 60 * 1000,
28 | })
29 | );
30 | app.use((req, res, next) => {
31 | res.sendFile(pathToIndexHtml);
32 | });
33 |
34 | app.server.listen(process.env.PORT || config.port, config.hostname || "localhost", () => {
35 | console.log(`Started on port ${ app.server.address().port }`);
36 | });
37 |
38 | export default app;
39 |
--------------------------------------------------------------------------------
/spa/tools/webpack/InlinePreStylesPlugin.js:
--------------------------------------------------------------------------------
1 | class InlinePreStylesPlugin {
2 | apply(compiler) {
3 | // Hook into the html-webpack-plugin processing
4 | compiler.plugin("compilation", compilation => {
5 | compilation.plugin("html-webpack-plugin-alter-asset-tags", (htmlPluginData, callback) => {
6 | const assets = compilation.assets;
7 | const styleTags = Object.keys(assets)
8 | .filter(file => /\.pre\.css$/.test(file) === true)
9 | .map(file => assets[file].source().toString("utf8"))
10 | .map(src => ({
11 | tagName: "style",
12 | closeTag: true,
13 | attributes: {
14 | type: "text/css",
15 | },
16 | innerHTML: src,
17 | }));
18 |
19 | htmlPluginData.head = htmlPluginData.head.concat(styleTags);
20 |
21 | callback(null, htmlPluginData);
22 | });
23 | });
24 | }
25 | }
26 |
27 | module.exports = InlinePreStylesPlugin;
28 |
--------------------------------------------------------------------------------
/spa/tools/webpack/exportCssLoader.js:
--------------------------------------------------------------------------------
1 | const exportCss = `module.exports = (({ renderStatic }, oldExports) => {
2 | const oldKeys = Object.keys(oldExports);
3 | const locals = {};
4 | const { css } = renderStatic(() => {
5 | oldKeys.forEach(key => {
6 | const exportValue = oldExports[key];
7 |
8 | if (exportValue !== undefined && exportValue !== null) {
9 | locals[key] = exportValue.toString();
10 | }
11 | });
12 |
13 | return "";
14 | });
15 | const newExports = [[module.id, css]];
16 |
17 | Object.assign(newExports, oldExports);
18 | newExports.locals = locals;
19 |
20 | return newExports;
21 | })(require("glamor/server"), module.exports);`;
22 |
23 | module.exports = function (source, sourceMaps) {
24 | this.callback(null, source + ";" + exportCss, sourceMaps);
25 | };
26 |
--------------------------------------------------------------------------------
/universal/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "development": {
4 | "presets": [
5 | [
6 | "env",
7 | {
8 | "targets": "current"
9 | }
10 | ]
11 | ]
12 | },
13 | "browser": {
14 | "presets": [
15 | [
16 | "env",
17 | {
18 | "targets": {
19 | "browsers": ["last 2 versions"]
20 | },
21 | "modules": false
22 | }
23 | ]
24 | ]
25 | }
26 | },
27 | "plugins": [
28 | [
29 | "transform-react-jsx",
30 | {
31 | "pragma": "h"
32 | }
33 | ],
34 | "transform-react-constant-elements",
35 | "syntax-dynamic-import",
36 | "transform-runtime",
37 | "transform-object-rest-spread"
38 | ],
39 | "ignore": ["dist/**", "node_modules/**"],
40 | "retainLines": true
41 | }
42 |
--------------------------------------------------------------------------------
/universal/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["peerigon"],
3 | "env": {
4 | "node": true,
5 | "browser": false
6 | },
7 | "root": true,
8 | "rules": {
9 | "no-mixed-operators": "off"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/universal/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | /public
61 | /dist
--------------------------------------------------------------------------------
/universal/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Debug app:client:build",
11 | "env": {
12 | "WEBPACK_ENV": "production",
13 | "WEBPACK_TARGET": "browser"
14 | },
15 | "program": "${workspaceRoot}/node_modules/.bin/webpack",
16 | "args": [
17 | "--config",
18 | "config/webpack.config.babel.js"
19 | ]
20 | },
21 | {
22 | "type": "node",
23 | "request": "launch",
24 | "name": "Debug server",
25 | "env": {},
26 | "program": "${workspaceRoot}/dist/server/index.js"
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/universal/app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["jsx-a11y"],
3 | "extends": ["peerigon/react", "plugin:jsx-a11y/recommended"],
4 | "settings": {
5 | "import/resolver": {
6 | "webpack": {
7 | "config": "config/webpack.config.babel.js"
8 | }
9 | }
10 | },
11 | "rules": {
12 | "class-methods-use-this": "off",
13 | // Prettier has issues with this.
14 | // Remove this rule once https://github.com/prettier/prettier/issues/1565 is resolved
15 | "newline-per-chained-call": "off",
16 | // We don't do proptype validation in this example
17 | "react/prop-types": "off",
18 | "react/no-unknown-property": [
19 | "error",
20 | {
21 | "ignore": ["class"]
22 | }
23 | ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/universal/app/assets/fonts/latoLatinLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/latoLatinLight.ttf
--------------------------------------------------------------------------------
/universal/app/assets/fonts/latoLatinLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/latoLatinLight.woff
--------------------------------------------------------------------------------
/universal/app/assets/fonts/latoLatinLight.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/latoLatinLight.woff2
--------------------------------------------------------------------------------
/universal/app/assets/fonts/nexaExtraboldWebfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/nexaExtraboldWebfont.ttf
--------------------------------------------------------------------------------
/universal/app/assets/fonts/nexaExtraboldWebfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/nexaExtraboldWebfont.woff
--------------------------------------------------------------------------------
/universal/app/assets/fonts/nexaExtraboldWebfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/nexaExtraboldWebfont.woff2
--------------------------------------------------------------------------------
/universal/app/assets/fonts/nexaHeavyWebfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/nexaHeavyWebfont.ttf
--------------------------------------------------------------------------------
/universal/app/assets/fonts/nexaHeavyWebfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/nexaHeavyWebfont.woff
--------------------------------------------------------------------------------
/universal/app/assets/fonts/nexaHeavyWebfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/fonts/nexaHeavyWebfont.woff2
--------------------------------------------------------------------------------
/universal/app/assets/img/peerigonLogoMint.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
11 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/universal/app/assets/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/public/favicon.ico
--------------------------------------------------------------------------------
/universal/app/assets/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/universal/app/assets/public/postImage1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/public/postImage1.jpg
--------------------------------------------------------------------------------
/universal/app/assets/public/postImage2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/public/postImage2.jpg
--------------------------------------------------------------------------------
/universal/app/assets/public/postImage3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/public/postImage3.jpg
--------------------------------------------------------------------------------
/universal/app/assets/public/postImage4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/public/postImage4.jpg
--------------------------------------------------------------------------------
/universal/app/assets/public/userImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/assets/public/userImage.jpg
--------------------------------------------------------------------------------
/universal/app/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/universal/app/client/captureFormSubmit.js:
--------------------------------------------------------------------------------
1 | import { state as routerState } from "../components/router/router";
2 |
3 | const toArray = Array.from.bind(Array);
4 | const formDataFilter = ["_method", "_csrf"];
5 |
6 | function collectFormData(formElement) {
7 | const formData = {};
8 |
9 | toArray(formElement.elements)
10 | .filter(({ name }) => name !== "" && formDataFilter.indexOf(name) === -1)
11 | .forEach(({ name, value }) => {
12 | formData[name] = value;
13 | });
14 |
15 | return formData;
16 | }
17 |
18 | export default function captureFormSubmit(store) {
19 | document.addEventListener("submit", event => {
20 | const formElement = event.target;
21 |
22 | event.preventDefault();
23 |
24 | store.dispatch(
25 | routerState.actions.push({
26 | method: formElement.elements._method.value,
27 | url: formElement.action,
28 | body: collectFormData(formElement),
29 | })
30 | );
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/universal/app/client/captureHistoryPop.js:
--------------------------------------------------------------------------------
1 | import onHistoryPop from "nanohistory";
2 | import { state as routerState } from "../components/router/router";
3 |
4 | export default function captureHistoryPop(store) {
5 | onHistoryPop(location => {
6 | store.dispatch(routerState.actions.enter(location.href));
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/universal/app/client/captureLinkClick.js:
--------------------------------------------------------------------------------
1 | import onLinkClick from "nanohref";
2 | import { state as routerState } from "../components/router/router";
3 |
4 | export default function captureLinkClick(store) {
5 | onLinkClick(node => {
6 | const url = node.href;
7 |
8 | if (node.hasAttribute("data-route") === false) {
9 | window.location = url;
10 |
11 | return;
12 | }
13 |
14 | const action = routerState.actions[node.hasAttribute("data-replace-url") ? "replace" : "push"];
15 |
16 | store.dispatch(action(url));
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/universal/app/client/index.js:
--------------------------------------------------------------------------------
1 | import { render, h } from "preact";
2 |
3 | window.h = h;
4 |
5 | function startApp() {
6 | const createApp = require("../createApp").default;
7 | const captureFormSubmit = require("./captureFormSubmit").default;
8 | const captureLinkClick = require("./captureLinkClick").default;
9 | const captureHistoryPop = require("./captureHistoryPop").default;
10 | const preloadChunkEntries = require("./preloadChunkEntries").default;
11 | const chunkState = require("../components/chunks/chunks").state;
12 | const storeState = require("../components/store/store").state;
13 |
14 | const initialState = window.__PRELOADED_STATE__ || {};
15 | const effectContext = {};
16 | const { app, store } = createApp(initialState, effectContext);
17 |
18 | store.dispatch(storeState.actions.hydrateStates());
19 |
20 | preloadChunkEntries(chunkState.select(store.getState()).loadedEntries).then(() => {
21 | render(app, document.body, document.body.firstElementChild);
22 |
23 | captureLinkClick(store);
24 | captureFormSubmit(store);
25 | captureHistoryPop(store);
26 | });
27 | }
28 |
29 | document.addEventListener("DOMContentLoaded", startApp);
30 |
--------------------------------------------------------------------------------
/universal/app/client/preloadChunkEntries.js:
--------------------------------------------------------------------------------
1 | export default function preloadChunkEntries(chunkEntries) {
2 | return Promise.all(chunkEntries.map(entry => entry.load()));
3 | }
4 |
--------------------------------------------------------------------------------
/universal/app/components/about/about.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import sheet from "../../styles/block/sheet";
4 | import {
5 | regularFontSize,
6 | regularLineHeight,
7 | regularMaxWidth,
8 | headlineFontSize,
9 | headlineLineHeight,
10 | } from "../../styles/typoSizes";
11 | import nexaHeavy from "../../styles/type/nexaHeavy";
12 | import latoLight from "../../styles/type/latoLight";
13 |
14 | export const root = css({
15 | position: "relative",
16 | });
17 |
18 | export const aboutSheet = css({
19 | ...sheet,
20 | maxWidth: regularMaxWidth + "rem",
21 | marginLeft: "auto",
22 | marginRight: "auto",
23 | });
24 |
25 | export const headline = css({
26 | ...nexaHeavy,
27 | fontSize: headlineFontSize + "rem",
28 | lineHeight: headlineLineHeight + "rem",
29 | ":not(:last-child)": {
30 | marginBottom: rem(10) + "rem",
31 | },
32 | });
33 |
34 | export const text = css({
35 | ...latoLight,
36 | fontSize: regularFontSize + "rem",
37 | lineHeight: regularLineHeight + "rem",
38 | });
39 |
--------------------------------------------------------------------------------
/universal/app/components/about/about.js:
--------------------------------------------------------------------------------
1 | import defineState from "../../store/defineState";
2 | import contexts from "../../contexts";
3 | import { aboutSheet, headline, text } from "./about.css";
4 |
5 | const name = "about";
6 |
7 | export const state = defineState({
8 | scope: name,
9 | context: contexts.state,
10 | actions: {
11 | enter: () => (getState, patchState, dispatchAction) => {},
12 | },
13 | });
14 |
15 | export default function About() {
16 | return (
17 |
18 |
19 |
About
20 |
21 |
22 | Delectus quia nulla sit ex ipsum sit animi incidunt. Nam rerum reiciendis et. Minus voluptatem
23 | natus mollitia temporibus. Molestias dolorem omnis eveniet repudiandae corporis voluptas sed
24 | quo.
25 |
26 |
27 | Quisquam a vel quia in quis blanditiis sed. Labore ratione minus. A quo consequuntur recusandae
28 | consequatur. Et aspernatur quod officia rem quam nisi vel est quidem.
29 |
30 |
31 |
32 | Alias et fugit error quaerat consequatur. Voluptatem omnis aut voluptatem. Et necessitatibus qui
33 | voluptatem.
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/universal/app/components/allPosts/allPosts.js:
--------------------------------------------------------------------------------
1 | import defineState from "../../store/defineState";
2 | import contexts from "../../contexts";
3 | import defineComponent from "../util/defineComponent";
4 | import getAll from "../../effects/api/posts/getAll";
5 | import Posts from "../posts/posts";
6 |
7 | const name = "allPosts";
8 |
9 | export const state = defineState({
10 | scope: name,
11 | context: contexts.state,
12 | initialState: {
13 | posts: null,
14 | },
15 | actions: {
16 | enter: () => (getState, patchState, dispatchAction, execEffect) =>
17 | execEffect(getAll).then(posts => {
18 | patchState({
19 | posts,
20 | });
21 | }),
22 | update: () => (getState, patchState, dispatchAction, execEffect) => Function.prototype,
23 | },
24 | });
25 |
26 | export default defineComponent({
27 | name,
28 | connectToStore: {
29 | watch: [state.select],
30 | },
31 | render(props, state) {
32 | return ;
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/universal/app/components/app/app.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { mintLight35, silverLight10, black } from "../../styles/colors";
3 | import { linear } from "../../styles/gradients";
4 | import { maxContentWidth } from "../../styles/layout";
5 | import { paddingRegular } from "../../styles/paddings";
6 |
7 | import "../../styles/reset"; // eslint-disable-line import/no-unassigned-import
8 |
9 | export const root = css({
10 | margin: 0,
11 | color: black(),
12 | backgroundImage: linear("to bottom", [silverLight10(), mintLight35() + " 70vh"]),
13 | minHeight: "100vh",
14 | });
15 |
16 | export const main = css({
17 | maxWidth: maxContentWidth + "rem",
18 | marginLeft: "auto",
19 | marginRight: "auto",
20 | ["@media (min-width: " + paddingRegular * 20 + "px)"]: {
21 | padding: paddingRegular,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/universal/app/components/app/app.js:
--------------------------------------------------------------------------------
1 | import { root, main } from "./app.css";
2 | import Header from "../header/header";
3 | import Store from "../store/store";
4 | import RoutePlaceholder from "../router/routePlaceholder";
5 | import Modal from "../modal/modal";
6 |
7 | export default function App(props) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/universal/app/components/chunks/chunks.js:
--------------------------------------------------------------------------------
1 | import defineState from "../../store/defineState";
2 | import { state as storeState } from "../store/store";
3 | import contexts from "../../contexts";
4 | import renderChild from "../util/renderChild";
5 |
6 | const name = "chunks";
7 |
8 | function addIfNecessary(chunkEntry, getState, patchState, dispatchAction) {
9 | const loadedEntries = getState().loadedEntries;
10 |
11 | if (loadedEntries.indexOf(chunkEntry) === -1) {
12 | patchState({
13 | loadedEntries: loadedEntries.concat(chunkEntry),
14 | });
15 |
16 | dispatchAction(storeState.actions.hydrateStates());
17 | }
18 | }
19 |
20 | export function selectLoadedChunks(contextState) {
21 | return state.select(contextState).loadedEntries.map(entries => entries.chunk);
22 | }
23 |
24 | export const state = defineState({
25 | scope: name,
26 | context: contexts.state,
27 | initialState: {
28 | loadedEntries: [],
29 | },
30 | hydrate(dehydrated) {
31 | return {
32 | ...dehydrated,
33 | loadedEntries: dehydrated.loadedEntries.map(id => contexts.chunkEntries[id]),
34 | toJSON() {
35 | return {
36 | ...this,
37 | loadedEntries: this.loadedEntries.map(entry => entry.id),
38 | };
39 | },
40 | };
41 | },
42 | actions: {
43 | import: chunkEntry => (getState, patchState, dispatchAction) => {
44 | const entryModule = chunkEntry.get();
45 |
46 | if (entryModule !== null) {
47 | // In case the entryModule is already loaded into the application,
48 | // we need to add it synchronously to the store because the import action
49 | // might have been triggered during a server render.
50 | addIfNecessary(chunkEntry, getState, patchState, dispatchAction);
51 |
52 | return Promise.resolve(entryModule);
53 | }
54 |
55 | return chunkEntry
56 | .load()
57 | .then(() => addIfNecessary(chunkEntry, getState, patchState, dispatchAction))
58 | .then(() => chunkEntry.get());
59 | },
60 | },
61 | });
62 |
63 | export default renderChild;
64 |
--------------------------------------------------------------------------------
/universal/app/components/chunks/defineChunkEntry.js:
--------------------------------------------------------------------------------
1 | import { state as chunkState } from "./chunks";
2 | import Placeholder from "../placeholder/placeholder";
3 | import defineComponent from "../util/defineComponent";
4 | import has from "../../util/has";
5 |
6 | export default function defineChunkEntry(descriptor) {
7 | const chunk = descriptor.chunk;
8 | const context = descriptor.context;
9 |
10 | if (typeof chunk !== "string") {
11 | throw new Error("Chunk name is missing");
12 | }
13 | if (context === undefined) {
14 | throw new Error("Chunk entry context is missing");
15 | }
16 |
17 | const id = has(descriptor, "name") ? descriptor.chunk + "/" + descriptor.name : descriptor.chunk;
18 |
19 | if (has(context, id)) {
20 | throw new Error(`Chunk entry ${ id } is already defined on given chunk entry context`);
21 | }
22 |
23 | const load = descriptor.load;
24 | let entryModule = null;
25 | let error = null;
26 | const ChunkEntryPlaceholder = defineComponent({
27 | connectToStore: {
28 | watch: [chunkState.select],
29 | mapToState: () => ({
30 | Component: entryModule === null ? error : entryModule.default,
31 | }),
32 | },
33 | render(props, state) {
34 | if (descriptor.placeholder) {
35 | return descriptor.placeholder(props, state);
36 | }
37 |
38 | return ;
39 | },
40 | });
41 | const chunkEntry = {
42 | id,
43 | chunk,
44 | get: () => entryModule,
45 | load: () => {
46 | if (entryModule !== null) {
47 | return Promise.resolve(entryModule);
48 | }
49 |
50 | return load().then(
51 | result => {
52 | error = null;
53 | entryModule = result;
54 |
55 | return result;
56 | },
57 | err => {
58 | error = err;
59 | entryModule = null;
60 |
61 | throw err;
62 | }
63 | );
64 | },
65 | Placeholder: ChunkEntryPlaceholder,
66 | };
67 |
68 | chunkEntry.import = chunkState.actions.import(chunkEntry);
69 |
70 | context[id] = chunkEntry;
71 |
72 | return chunkEntry;
73 | }
74 |
--------------------------------------------------------------------------------
/universal/app/components/document/document.js:
--------------------------------------------------------------------------------
1 | import contexts from "../../contexts";
2 | import defineState from "../../store/defineState";
3 | import renderChild from "../util/renderChild";
4 | import document from "../../effects/document";
5 |
6 | export const state = defineState({
7 | scope: "document",
8 | context: contexts.state,
9 | initialState: {
10 | statusCode: null,
11 | title: null,
12 | headerTags: null,
13 | },
14 | actions: {
15 | update: state => (getState, patchState, dispatchAction, execEffect) => {
16 | patchState(state);
17 |
18 | const newState = getState();
19 |
20 | execEffect(document.setTitle, newState.title);
21 | },
22 | },
23 | });
24 |
25 | export default renderChild;
26 |
--------------------------------------------------------------------------------
/universal/app/components/error/error.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import sheet from "../../styles/block/sheet";
4 | import {
5 | regularFontSize,
6 | regularLineHeight,
7 | regularMaxWidth,
8 | headlineFontSize,
9 | headlineLineHeight,
10 | } from "../../styles/typoSizes";
11 | import nexaHeavy from "../../styles/type/nexaHeavy";
12 | import latoLight from "../../styles/type/latoLight";
13 |
14 | export const root = css({
15 | position: "relative",
16 | });
17 |
18 | export const errorSheet = css({
19 | ...sheet,
20 | maxWidth: regularMaxWidth + "rem",
21 | marginLeft: "auto",
22 | marginRight: "auto",
23 | });
24 |
25 | export const headline = css({
26 | ...nexaHeavy,
27 | fontSize: headlineFontSize + "rem",
28 | lineHeight: headlineLineHeight + "rem",
29 | ":not(:last-child)": {
30 | marginBottom: rem(10) + "rem",
31 | },
32 | });
33 |
34 | export const text = css({
35 | ...latoLight,
36 | fontSize: regularFontSize + "rem",
37 | lineHeight: regularLineHeight + "rem",
38 | });
39 |
--------------------------------------------------------------------------------
/universal/app/components/error/error.js:
--------------------------------------------------------------------------------
1 | import defineState from "../../store/defineState";
2 | import defineComponent from "../util/defineComponent";
3 | import contexts from "../../contexts";
4 | import { errorSheet, headline, text } from "./error.css";
5 | import has from "../../util/has";
6 |
7 | const name = "error";
8 |
9 | export const state = defineState({
10 | scope: name,
11 | context: contexts.state,
12 | initialState: {
13 | headline: null,
14 | message: null,
15 | },
16 | actions: {
17 | enter: (request, route, params) => (getState, patchState, dispatchAction) => {
18 | const headline = has(params, "title") ? params.title : "Error";
19 | const message = has(params, "message") ? params.message : "An unexpected error occurred";
20 |
21 | patchState({
22 | headline,
23 | message,
24 | });
25 | },
26 | },
27 | });
28 |
29 | export default defineComponent({
30 | name,
31 | connectToStore: {
32 | watch: [state.select],
33 | },
34 | render(props, state) {
35 | return (
36 |
37 |
38 |
39 | {state.headline}
40 |
41 |
42 |
43 | {state.message}
44 |
45 |
46 |
47 |
48 | );
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/universal/app/components/form/form.js:
--------------------------------------------------------------------------------
1 | import has from "../../util/has";
2 | import filterProps from "../../util/filterProps";
3 | import renderUrl from "../../util/renderUrl";
4 | import defineComponent from "../util/defineComponent";
5 | import { state as routerState } from "../router/router";
6 |
7 | const name = "form";
8 | const ownProps = ["method", "csrfToken", "actionRoute", "actionParams"];
9 | const filterDangerousParams = ["next", "previous", "form"];
10 | const emptyObj = {};
11 |
12 | export default defineComponent({
13 | name,
14 | connectToStore: {
15 | watch: [routerState.select],
16 | mapToState: ({ request }) => ({
17 | currentUrl: request.url,
18 | }),
19 | },
20 | render(props, state) {
21 | const { actionParams = emptyObj } = props;
22 | const method = has(props, "method") ? props.method.toUpperCase() : "GET";
23 | const formProps = filterProps(props, ownProps);
24 | const isNonGET = method !== "GET";
25 | // Do not extend the action params with pervious and next parameters
26 | // when it's a GET request because these requests can be crafted by an attacker.
27 | // All the other requests require a valid csrf token. As soon as the request
28 | // hits our route handler, we can expect it to be safe.
29 | const extendedActionParams = isNonGET ?
30 | {
31 | ...actionParams,
32 | previous: has(actionParams, "previous") ? actionParams.previous : state.currentUrl,
33 | next: has(actionParams, "next") ? actionParams.next : state.currentUrl,
34 | form: has(props, "name") ? props.name : null,
35 | } :
36 | filterProps(actionParams, filterDangerousParams);
37 |
38 | // HTML forms only support GET and POST
39 | // The actual method is encoded as _method param
40 | return (
41 |
50 | );
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/universal/app/components/formFeedback/formFeedback.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { asideFontSize, asideLineHeight } from "../../styles/typoSizes";
3 | import { red } from "../../styles/colors";
4 | import latoLight from "../../styles/type/latoLight";
5 | import { msToSeconds } from "../../styles/timing";
6 |
7 | export const transitionDuration = 100;
8 |
9 | const transitionDurationCss = msToSeconds(100) + "s";
10 |
11 | export const overflowContainer = css({
12 | display: "inline-block",
13 | overflow: "hidden",
14 | transition: `height ${ transitionDurationCss } ease-in-out`,
15 | });
16 |
17 | export const message = css({
18 | ...latoLight,
19 | display: "inline-block",
20 | color: red(),
21 | fontSize: asideFontSize + "rem",
22 | lineHeight: asideLineHeight + "rem",
23 | minHeight: asideLineHeight + "rem",
24 | transform: "translateY(0)",
25 | transition: `transform ${ transitionDurationCss } ease-in-out`,
26 | ":empty": {
27 | transform: "translateY(-100%)",
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/universal/app/components/formFeedback/formFeedback.js:
--------------------------------------------------------------------------------
1 | import { overflowContainer, message } from "./formFeedback.css";
2 |
3 | export default function FormFeedback(props) {
4 | const styles = { ...overflowContainer, ...props };
5 |
6 | return (
7 |
8 |
9 | {props.children}
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/universal/app/components/header/common.js:
--------------------------------------------------------------------------------
1 | import { rem } from "../../styles/scales";
2 |
3 | // The type looks more vertically centered with this offset
4 | export const logoHeight = rem(17);
5 | export const headerCollapseBreakpoint = "@media (max-width: 35rem)";
6 |
--------------------------------------------------------------------------------
/universal/app/components/header/header.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { white, black } from "../../styles/colors";
3 | import { px, rem } from "../../styles/scales";
4 | import nexaHeavy from "../../styles/type/nexaHeavy";
5 | import { maxContentWidth } from "../../styles/layout";
6 | import { offscreen as a11yOffscreen } from "../../styles/a11y";
7 | import { header as headerZIndex } from "../../styles/zIndex";
8 | import { logoHeight, headerCollapseBreakpoint } from "./common";
9 |
10 | export const root = css({
11 | position: "sticky",
12 | top: 0,
13 | zIndex: headerZIndex,
14 | color: black(),
15 | backgroundColor: white(),
16 | boxShadow: "0 5px 5px rgba(0, 0, 0, 0.1)",
17 | });
18 |
19 | export const content = css({
20 | display: "flex",
21 | alignItems: "center",
22 | lineHeight: logoHeight + "rem",
23 | flexWrap: "wrap",
24 | padding: px(6),
25 | maxWidth: maxContentWidth + "rem",
26 | marginLeft: "auto",
27 | marginRight: "auto",
28 | });
29 |
30 | export const logo = css({
31 | display: "flex",
32 | alignItems: "center",
33 | textDecoration: "none",
34 | color: "currentColor",
35 | });
36 |
37 | export const nav = css({
38 | marginLeft: rem(12) + "rem",
39 | [headerCollapseBreakpoint]: {
40 | marginLeft: 0,
41 | width: "100%",
42 | order: 1,
43 | },
44 | });
45 |
46 | export const session = css({
47 | marginLeft: "auto",
48 | });
49 |
50 | export const headline = css({
51 | ...nexaHeavy,
52 | fontSize: rem(13) + "rem",
53 | margin: 0,
54 | marginLeft: px(10),
55 | });
56 |
57 | export const offscreen = css(a11yOffscreen);
58 |
--------------------------------------------------------------------------------
/universal/app/components/header/header.js:
--------------------------------------------------------------------------------
1 | import Logo from "./logo/logo";
2 | import Nav from "./nav/nav";
3 | import Link from "../router/link";
4 | import routes from "../../routes";
5 | import { root, content, logo, nav, headline, session, offscreen } from "./header.css";
6 | import defineComponent from "../util/defineComponent";
7 | import Session from "./session/session";
8 |
9 | export default defineComponent({
10 | name: "Header",
11 | render() {
12 | return (
13 |
25 | );
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/universal/app/components/header/link.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px, rem } from "../../styles/scales";
3 | import nexaXBold from "../../styles/type/nexaXBold";
4 | import { regular as regularBorder } from "../../styles/borders";
5 |
6 | const activeLinkStyles = {
7 | borderTop: regularBorder("transparent"),
8 | borderBottom: regularBorder(),
9 | };
10 |
11 | export const activeLink = css(activeLinkStyles);
12 |
13 | export const link = css({
14 | ...nexaXBold,
15 | // There's a small baseline correction necessary
16 | position: "relative",
17 | top: 1,
18 | color: "currentColor",
19 | fontSize: rem(12) + "rem",
20 | textDecoration: "none",
21 | padding: `2px ${ px(5) }px`,
22 | cursor: "pointer",
23 | ":hover": activeLinkStyles,
24 | ":active": activeLinkStyles,
25 | });
26 |
--------------------------------------------------------------------------------
/universal/app/components/header/logo/logo.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { logoHeight } from "../common";
3 |
4 | export const logoImg = css({
5 | position: "relative",
6 | display: "block",
7 | height: logoHeight + "rem",
8 | });
9 |
--------------------------------------------------------------------------------
/universal/app/components/header/logo/logo.js:
--------------------------------------------------------------------------------
1 | import logoSrc from "../../../assets/img/peerigonLogoMint.svg";
2 | import { logoImg } from "./logo.css";
3 |
4 | export default function Logo() {
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/universal/app/components/header/nav/nav.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px } from "../../../styles/scales";
3 |
4 | export const list = css({
5 | display: "flex",
6 | listStyleType: "none",
7 | });
8 |
9 | export const listItem = css({
10 | ":not(:last-child)": {
11 | marginRight: px(10),
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/universal/app/components/header/nav/nav.js:
--------------------------------------------------------------------------------
1 | import Link from "../../router/link";
2 | import { list, listItem } from "./nav.css";
3 | import { link, activeLink } from "../link.css";
4 | import routes from "../../../routes";
5 | import { nbsp } from "../../../util/htmlEntities";
6 |
7 | export default function Nav(props) {
8 | return (
9 |
10 |
11 |
12 |
13 | Top{nbsp}5
14 |
15 |
16 |
17 |
18 | All
19 |
20 |
21 |
22 |
23 | About
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/universal/app/components/header/session/anonymous/anonymous.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../../../util/defineComponent";
2 | import { state as routerState } from "../../../router/router";
3 | import { link } from "../../link.css";
4 | import { nbsp } from "../../../../util/htmlEntities";
5 | import ModalLink from "../../../modal/modalLink";
6 | import loginForm from "../../../loginForm";
7 | import renderUrl from "../../../../util/renderUrl";
8 | import filterProps from "../../../../util/filterProps";
9 |
10 | const name = "headerSessionAnonymous";
11 | const LoginFormPlaceholder = loginForm.Placeholder;
12 | const emptyObj = {};
13 | const triggerParam = "showLogin";
14 |
15 | export default defineComponent({
16 | name,
17 | connectToStore: {
18 | watch: [routerState.select],
19 | mapToState: ({ request, route, params }) => ({
20 | nextUrlAfterLogin: renderUrl(route.url, filterProps(params, [triggerParam])),
21 | }),
22 | },
23 | render(props, state) {
24 | return (
25 |
26 | }
28 | triggerParam={triggerParam}
29 | importAction={loginForm.import}
30 | {...link}
31 | >
32 | Log{nbsp}in
33 |
34 |
35 | );
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/universal/app/components/header/session/personal/personal.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px } from "../../../../styles/scales";
3 | import latoLight from "../../../../styles/type/latoLight";
4 | import { headerCollapseBreakpoint, logoHeight } from "../../common";
5 | import { regularFontSize } from "../../../../styles/typoSizes";
6 |
7 | export const root = css({
8 | display: "flex",
9 | alignItems: "center",
10 | fontSize: regularFontSize + "rem",
11 | "> *:not(:last-child)": {
12 | marginRight: px(10),
13 | },
14 | });
15 |
16 | export const userName = css({
17 | ...latoLight,
18 | [headerCollapseBreakpoint]: {
19 | display: "none",
20 | },
21 | });
22 |
23 | export const userImage = css({
24 | display: "block",
25 | height: logoHeight + "rem",
26 | borderRadius: "100%",
27 | });
28 |
--------------------------------------------------------------------------------
/universal/app/components/header/session/personal/personal.js:
--------------------------------------------------------------------------------
1 | import { root, userName, userImage } from "./personal.css";
2 | import { link } from "../../link.css";
3 | import defineForm from "../../../form/defineForm";
4 | import defineComponent from "../../../util/defineComponent";
5 | import Form from "../../../form/form";
6 | import contexts from "../../../../contexts";
7 | import routes from "../../../../routes";
8 |
9 | const logoutForm = defineForm({
10 | name: "logoutForm",
11 | context: contexts.state,
12 | });
13 |
14 | const LogoutForm = defineComponent({
15 | name: "LogoutForm",
16 | connectToStore: {
17 | watch: [logoutForm.select],
18 | mapToState: ({ csrfToken }) => ({
19 | csrfToken,
20 | }),
21 | },
22 | render(props, { csrfToken }) {
23 | return (
24 |
27 | );
28 | },
29 | });
30 |
31 | export default function HeaderSessionPersonal(props) {
32 | const user = props.user;
33 |
34 | if (user === null) {
35 | return null;
36 | }
37 |
38 | return (
39 |
40 |
41 |
42 | {user.name}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/universal/app/components/header/session/session.js:
--------------------------------------------------------------------------------
1 | import { state as sessionState } from "../../session/session";
2 | import Personal from "./personal/personal";
3 | import Anonymous from "./anonymous/anonymous";
4 | import defineComponent from "../../util/defineComponent";
5 | import has from "../../../util/has";
6 |
7 | const name = "headerSession";
8 | const emptyObj = {};
9 |
10 | export default defineComponent({
11 | name,
12 | connectToStore: {
13 | watch: [sessionState.select],
14 | mapToState: ({ user }) => ({
15 | user,
16 | isLoggedIn: user !== null,
17 | }),
18 | },
19 | render(props, state) {
20 | const user = state.user;
21 | const styles = has(props, "styles") ? props.styles : emptyObj;
22 |
23 | return (
24 |
25 | {state.isLoggedIn ?
:
}
26 |
27 | );
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/universal/app/components/loading/loading.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import latoLight from "../../styles/type/latoLight";
3 | import { rem } from "../../styles/scales";
4 |
5 | export const root = css({
6 | ...latoLight,
7 | fontSize: rem(13) + "rem",
8 | display: "flex",
9 | width: "100%",
10 | height: "100%",
11 | minHeight: rem(20) + "rem",
12 | alignItems: "center",
13 | justifyContent: "center",
14 | });
15 |
--------------------------------------------------------------------------------
/universal/app/components/loading/loading.js:
--------------------------------------------------------------------------------
1 | import { root } from "./loading.css";
2 |
3 | export const loading = Loading...
;
4 |
5 | export default function Loading() {
6 | return loading;
7 | }
8 |
--------------------------------------------------------------------------------
/universal/app/components/loginForm/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | chunk: "session",
6 | name: "loginForm",
7 | context: contexts.chunkEntries,
8 | load: () => import("./loginForm" /* webpackChunkName: "session" */),
9 | });
10 |
--------------------------------------------------------------------------------
/universal/app/components/loginForm/loginForm.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import sheet from "../../styles/block/sheet";
4 | import { regularFontSize, regularLineHeight } from "../../styles/typoSizes";
5 | import latoLight from "../../styles/type/latoLight";
6 | import inputText from "../../styles/block/inputText";
7 | import inputSubmit from "../../styles/block/inputSubmit";
8 | import { paddingBigger } from "../../styles/paddings";
9 |
10 | export const loginSheet = css({
11 | ...sheet,
12 | ...latoLight,
13 | maxWidth: rem(29) + "rem",
14 | boxSizing: "border-box",
15 | padding: paddingBigger,
16 | fontSize: regularFontSize + "rem",
17 | lineHeight: regularLineHeight + "rem",
18 | display: "flex",
19 | flexDirection: "column",
20 | });
21 |
22 | export const loginLabel = css({
23 | ":after": {
24 | content: JSON.stringify(":"),
25 | },
26 | });
27 |
28 | export const loginInput = css({
29 | ...inputText,
30 | });
31 |
32 | export const formFeedback = css({
33 | marginBottom: rem(10) + "rem",
34 | });
35 |
36 | export const loginSubmit = css(inputSubmit);
37 |
--------------------------------------------------------------------------------
/universal/app/components/loginForm/loginFormValidators.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: formData => {
3 | const name = formData.name;
4 |
5 | if (name === "") {
6 | return "Missing login name";
7 | }
8 |
9 | return null;
10 | },
11 | password: formData => {
12 | const password = formData.password;
13 |
14 | if (password === "") {
15 | return "Missing password";
16 | }
17 |
18 | return null;
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/universal/app/components/modal/modal.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import hexToRgba from "hex-to-rgba";
3 | import { backdrop as backdropZIndex, modal as modalZIndex } from "../../styles/zIndex";
4 | import { msToSeconds } from "../../styles/timing";
5 | import { white, mint } from "../../styles/colors";
6 | import { paddingRegular } from "../../styles/paddings";
7 | import calc from "../../styles/calc";
8 |
9 | export const fadeDuration = 100;
10 | const fadeDurationCss = msToSeconds(fadeDuration) + "s";
11 | const backdropOpacity = 0.4;
12 |
13 | const backdropGlowAnimation = css.keyframes({
14 | "50%": {
15 | backgroundColor: hexToRgba(mint(), "0.2"),
16 | },
17 | });
18 |
19 | export const root = css({
20 | position: "absolute",
21 | top: 0,
22 | width: "100vw",
23 | height: "100vh",
24 | });
25 |
26 | export const rootHidden = css({
27 | transition: `transform 0s ${ fadeDurationCss }`,
28 | transform: "translateY(-100%)",
29 | });
30 |
31 | export const rootVisible = css({});
32 |
33 | export const backdrop = css({
34 | position: "fixed",
35 | top: 0,
36 | left: 0,
37 | right: 0,
38 | bottom: 0,
39 | zIndex: backdropZIndex,
40 | backgroundColor: "black",
41 | ":focus": {
42 | animation: `${ backdropGlowAnimation } infinite 3s ease-in-out`,
43 | },
44 | });
45 |
46 | export const backdropHidden = css({
47 | opacity: 0,
48 | transition: `opacity ${ fadeDurationCss } ease-in-out, transform 0s ${ fadeDurationCss }`,
49 | transform: "translateY(-100%)",
50 | });
51 |
52 | export const backdropVisible = css({
53 | opacity: backdropOpacity,
54 | transition: `opacity ${ fadeDurationCss } ease-in-out`,
55 | });
56 |
57 | export const window = css({
58 | position: "fixed",
59 | zIndex: modalZIndex,
60 | top: "50%",
61 | left: "50%",
62 | transform: "translate(-50%, -50%)",
63 | backgroundColor: white(),
64 | boxShadow: "0 7px 7px rgba(0, 0, 0, 0.3)",
65 | "> *": {
66 | width: calc("100vw - ", paddingRegular * 2, "px"),
67 | },
68 | });
69 |
--------------------------------------------------------------------------------
/universal/app/components/modal/modal.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../util/defineComponent";
2 | import contexts from "../../contexts";
3 | import defineState from "../../store/defineState";
4 | import Link from "../router/link";
5 | import { root, rootVisible, rootHidden, window, backdrop, backdropVisible, backdropHidden } from "./modal.css";
6 |
7 | const name = "modal";
8 |
9 | function isCurrentlyActive(state, component) {
10 | const current = state.component;
11 |
12 | return current !== null && current === component;
13 | }
14 |
15 | export const state = defineState({
16 | scope: name,
17 | context: contexts.state,
18 | initialState: {
19 | component: null,
20 | backUrl: "",
21 | },
22 | hydrate(dehydrated) {
23 | return {
24 | ...dehydrated,
25 | toJSON: () => undefined,
26 | };
27 | },
28 | actions: {
29 | show: (component, backUrl = "") => (getState, patchState, dispatchAction) => {
30 | patchState({
31 | component,
32 | backUrl,
33 | });
34 | },
35 | hide: component => (getState, patchState, dispatchAction) => {
36 | if (isCurrentlyActive(getState(), component) === true) {
37 | patchState({
38 | component: null,
39 | backUrl: "",
40 | });
41 | }
42 | },
43 | },
44 | });
45 |
46 | export default defineComponent({
47 | name,
48 | connectToStore: {
49 | watch: [state.select],
50 | mapToState: ({ component, backUrl }, oldState) => ({
51 | active: component !== null,
52 | component,
53 | backUrl,
54 | }),
55 | },
56 | render(props, state) {
57 | const rootStyles = {
58 | ...root,
59 | ...(state.active === true ? rootVisible : rootHidden),
60 | };
61 | const backdropStyles = {
62 | ...backdrop,
63 | ...(state.active === true ? backdropVisible : backdropHidden),
64 | };
65 |
66 | return (
67 |
68 |
69 |
70 | {state.component}
71 |
72 |
73 | );
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/universal/app/components/modal/modalLink.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../util/defineComponent";
2 | import Link from "../router/link";
3 | import ModalTrigger from "./modalTrigger";
4 | import filterProps from "../../util/filterProps";
5 | import { state as routerState } from "../router/router";
6 |
7 | const name = "modalLink";
8 | const ownProps = ["triggerParam", "importAction", "modal", "children"];
9 |
10 | export default defineComponent({
11 | name,
12 | connectToStore: {
13 | watch: [routerState.select],
14 | mapToState: ({ params }) => ({
15 | params,
16 | }),
17 | },
18 | render(props, state) {
19 | const linkProps = filterProps(props, ownProps);
20 | const additionalParams = Object.assign({}, state.params, props.additionalParams);
21 |
22 | additionalParams[props.triggerParam] = 1;
23 |
24 | return (
25 |
26 | {props.children}
27 |
28 | {props.modal}
29 |
30 |
31 | );
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/universal/app/components/modal/modalTrigger.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../util/defineComponent";
2 | import { state as routerState } from "../router/router";
3 | import { state as modalState } from "./modal";
4 | import has from "../../util/has";
5 | import renderUrl from "../../util/renderUrl";
6 | import filterProps from "../../util/filterProps";
7 |
8 | const name = "modalTrigger";
9 |
10 | export default defineComponent({
11 | name,
12 | connectToStore: {
13 | watch: [routerState.select],
14 | mapToState: ({ request, route, params }, { triggerParam }, oldState) => {
15 | const isErrorRoute = route.error === true;
16 | const skipStateChange = request.method !== "GET" && isErrorRoute === false;
17 |
18 | if (skipStateChange) {
19 | return oldState;
20 | }
21 |
22 | return {
23 | shouldBeActive: parseInt(params[triggerParam]) === 1,
24 | backUrl: renderUrl(route.url, filterProps(params, [triggerParam])),
25 | };
26 | },
27 | },
28 | willUpdate(props, state, dispatchAction) {
29 | const childComponent = props.children[0];
30 |
31 | if (state.shouldBeActive) {
32 | if (has(props, "importAction")) {
33 | dispatchAction(props.importAction);
34 | }
35 | dispatchAction(modalState.actions.show(childComponent, state.backUrl));
36 | } else {
37 | dispatchAction(modalState.actions.hide(childComponent));
38 | }
39 | },
40 | render() {
41 | return null;
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/universal/app/components/placeholder/placeholder.js:
--------------------------------------------------------------------------------
1 | import Loading from "../loading/loading";
2 | import defineComponent from "../util/defineComponent";
3 |
4 | const name = "placeholder";
5 |
6 | export default defineComponent({
7 | name,
8 | render({ children, Component = null, props = {} }) {
9 | const noChildren = children.length === 0;
10 |
11 | if (Component === null && noChildren) {
12 | return ;
13 | }
14 | if (Component !== null && Component instanceof Error === false) {
15 | return ;
16 | }
17 |
18 | const err = Component;
19 |
20 | if (err !== null && noChildren) {
21 | return (
22 |
23 | {Component.message}
24 |
25 | );
26 | }
27 |
28 | const childGenerator = children[0];
29 |
30 | return childGenerator(err);
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/universal/app/components/posts/post/post.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import nexaHeavy from "../../../styles/type/nexaHeavy";
3 | import latoLight from "../../../styles/type/latoLight";
4 | import { rem } from "../../../styles/scales";
5 | import {
6 | regularFontSize,
7 | regularLineHeight,
8 | regularMaxWidth,
9 | headlineFontSize,
10 | headlineLineHeight,
11 | headlineMaxWidth,
12 | } from "../../../styles/typoSizes";
13 |
14 | export const headline = css({
15 | ...nexaHeavy,
16 | fontSize: headlineFontSize + "rem",
17 | lineHeight: headlineLineHeight + "rem",
18 | maxWidth: headlineMaxWidth + "rem",
19 | marginBottom: rem(6) + "rem",
20 | });
21 |
22 | export const aside = css({
23 | ...latoLight,
24 | display: "block",
25 | fontSize: rem(11) + "rem",
26 | lineHeight: rem(12) + "rem",
27 | marginBottom: rem(13) + "rem",
28 | });
29 |
30 | export const paragraph = css({
31 | ...latoLight,
32 | fontSize: regularFontSize + "rem",
33 | lineHeight: regularLineHeight + "rem",
34 | maxWidth: regularMaxWidth + "rem",
35 | ":not(:last-child)": {
36 | marginBottom: rem(10) + "rem",
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/universal/app/components/posts/post/post.js:
--------------------------------------------------------------------------------
1 | import fromNow from "from-now";
2 | import defineComponent from "../../util/defineComponent";
3 | import { headline, paragraph, aside } from "./post.css";
4 | import has from "../../../util/has";
5 |
6 | const name = "postsPost";
7 | const lineBreak = /\s*[\r\n]+\s*/g;
8 | const emptyObj = {};
9 |
10 | export default defineComponent({
11 | name,
12 | render(props) {
13 | const post = props.post;
14 | const styles = has(props, "styles") ? props.styles : emptyObj;
15 |
16 | return (
17 |
18 |
19 | {post.title}
20 |
21 |
22 |
23 | {fromNow(post.published)}
24 |
25 | {" ago by "}
26 | {post.author}
27 |
28 |
29 | {post.content.split(lineBreak).map(p =>
30 | (
31 | {p}
32 |
)
33 | )}
34 |
35 |
36 | );
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/universal/app/components/posts/posts.css.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { px, rem } from "../../styles/scales";
3 | import { offscreen } from "../../styles/a11y";
4 | import { regularMaxWidth } from "../../styles/typoSizes";
5 | import sheet, { sheetPadding } from "../../styles/block/sheet";
6 | import attrSelector from "../util/attrSelector";
7 |
8 | export const root = css({
9 | position: "relative",
10 | });
11 |
12 | export const a11yTitle = css({
13 | ...offscreen,
14 | });
15 |
16 | export const postImage = css({
17 | position: "absolute",
18 | maxWidth: px(30),
19 | marginTop: sheetPadding,
20 | transform: "translate(0%)",
21 | transition: "transform 0.3s ease-in-out",
22 | });
23 |
24 | export const postSheet = css({
25 | ...sheet,
26 | // position relative is necessary so that the position absolute image is still below the sheet
27 | position: "relative",
28 | maxWidth: regularMaxWidth + "rem",
29 | });
30 |
31 | export const postContainer = css({
32 | position: "relative",
33 | overflow: "hidden",
34 | ":not(:last-child)": {
35 | marginBottom: rem(15) + "rem",
36 | },
37 | [":nth-child(odd) " + attrSelector(postSheet)]: {
38 | marginLeft: "auto",
39 | },
40 | [":nth-child(odd) " + attrSelector(postImage)]: {
41 | left: px(17),
42 | },
43 | [":nth-child(even) " + attrSelector(postImage)]: {
44 | right: px(17),
45 | },
46 | [":nth-child(odd):not(:hover) " + attrSelector(postImage)]: {
47 | transform: "translate(10%)",
48 | },
49 | [":nth-child(even):not(:hover) " + attrSelector(postImage)]: {
50 | transform: "translate(-10%)",
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/universal/app/components/posts/posts.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../util/defineComponent";
2 | import Post from "./post/post";
3 | import { a11yTitle, root, postContainer, postSheet, postImage } from "./posts.css";
4 |
5 | const name = "posts";
6 | const empty = [];
7 |
8 | export default defineComponent({
9 | name,
10 | render(props) {
11 | const posts = props.posts;
12 |
13 | return (
14 |
15 |
16 | {props.a11yTitle}
17 |
18 | {Array.isArray(posts) === false || posts.length === 0 ?
19 | empty :
20 | posts.map(post =>
21 | (
22 |
23 |
24 |
)
25 | )}
26 |
27 | );
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/universal/app/components/router/errors/methodNotAllowed.js:
--------------------------------------------------------------------------------
1 | export default function methodNotAllowed(allowedMethods, requestPathname) {
2 | return {
3 | statusCode: 405,
4 | title: "Method not allowed",
5 | message: `Only ${ allowedMethods.join(", ") } is allowed at ${ requestPathname }.`,
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/universal/app/components/router/link.js:
--------------------------------------------------------------------------------
1 | import hookIntoEvent from "../util/hookIntoEvent";
2 | import renderUrl from "../../util/renderUrl";
3 | import defineComponent from "../util/defineComponent";
4 | import { state as routerState } from "./router";
5 | import filterProps from "../../util/filterProps";
6 | import has from "../../util/has";
7 |
8 | const emptyObj = {};
9 | const emptyArr = [];
10 | const ownProps = [
11 | "route",
12 | "params",
13 | "children",
14 | "activeClass",
15 | "replaceRoute",
16 | "additionalParams",
17 | "withoutParams",
18 | "preloadAction",
19 | ];
20 |
21 | function dispatchPreloadAction(dispatchAction, event, props, state) {
22 | dispatchAction(state.preloadAction);
23 | }
24 |
25 | export default defineComponent({
26 | name: "Link",
27 | connectToStore: {
28 | watch: [routerState.select],
29 | mapToState: (routerState, props) => {
30 | const route = has(props, "route") ? props.route : routerState.route;
31 |
32 | return {
33 | url: routerState.request.url,
34 | route,
35 | params: routerState.params,
36 | isActive: route === routerState.route,
37 | preloadAction: has(props, "preloadAction") ? props.preloadAction : route.entry,
38 | };
39 | },
40 | },
41 | handlers: {
42 | handleMouseOver: hookIntoEvent("onMouseOver", dispatchPreloadAction),
43 | handleFocus: hookIntoEvent("onFocus", dispatchPreloadAction),
44 | },
45 | render(props, state) {
46 | const anchorProps = filterProps(props, ownProps);
47 | const {
48 | params = emptyObj,
49 | additionalParams,
50 | withoutParams = emptyArr,
51 | replaceRoute = false,
52 | href,
53 | children,
54 | activeClass,
55 | } = props;
56 | const route = state.route;
57 | let finalHref = href;
58 |
59 | if (href === undefined) {
60 | const finalParams = filterProps(Object.assign({}, params, additionalParams), withoutParams);
61 |
62 | finalHref = renderUrl(route.url, finalParams);
63 | }
64 |
65 | return (
66 |
75 | {children}
76 |
77 | );
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/universal/app/components/router/routePlaceholder.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../util/defineComponent";
2 | import { state as routerState } from "./router";
3 |
4 | const name = "routePlaceholder";
5 |
6 | export default defineComponent({
7 | name,
8 | connectToStore: {
9 | watch: [routerState.select],
10 | mapToState: ({ request, route, params }, props, oldState) => {
11 | const Component = route.placeholder === undefined ? null : route.placeholder(request, route, params);
12 |
13 | if (Component === null) {
14 | return oldState;
15 | }
16 |
17 | return {
18 | component: ,
19 | };
20 | },
21 | },
22 | render(props, state) {
23 | return state.component;
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/universal/app/components/router/router.js:
--------------------------------------------------------------------------------
1 | import defineState from "../../store/defineState";
2 | import contexts from "../../contexts";
3 | import renderChild from "../util/renderChild";
4 | import has from "../../util/has";
5 | import routes from "../../routes";
6 | import history from "../../effects/history";
7 | import changeRoute from "./util/changeRoute";
8 | import enterRoute from "./util/enterRoute";
9 | import sanitizeRequest from "./util/sanitizeRequest";
10 |
11 | const name = "router";
12 |
13 | function isCurrentGetRequest(state, request) {
14 | return state.request !== null && state.request.method === "GET" && state.request.url === request.url;
15 | }
16 |
17 | export const state = defineState({
18 | scope: name,
19 | context: contexts.state,
20 | initialState: {
21 | request: null,
22 | route: null,
23 | params: null,
24 | },
25 | hydrate(dehydrated) {
26 | const route = dehydrated.route;
27 |
28 | return {
29 | ...dehydrated,
30 | route: route !== null && has(routes, route.name) ? routes[route.name] : null,
31 | };
32 | },
33 | actions: {
34 | push: changeRoute({ abortIf: isCurrentGetRequest, historyEffect: history.push }),
35 | replace: changeRoute({ abortIf: isCurrentGetRequest, historyEffect: history.replace }),
36 | enter: enterRoute,
37 | reset: url => (getState, patchState, dispatchAction, execEffect) => {
38 | const request = sanitizeRequest(url);
39 |
40 | return Promise.resolve(execEffect(history.reset, request.url));
41 | },
42 | },
43 | });
44 |
45 | export default renderChild;
46 |
--------------------------------------------------------------------------------
/universal/app/components/router/util/changeRoute.js:
--------------------------------------------------------------------------------
1 | import sanitizeRequest from "./sanitizeRequest";
2 | import resolveRouteAndParams from "./resolveRouteAndParams";
3 | import enterRoute from "./enterRoute";
4 |
5 | export default function changeRoute({ abortIf, historyEffect }) {
6 | return (request, statusCode) => (getState, patchState, dispatchAction, execEffect) =>
7 | new Promise(resolve => {
8 | const oldState = getState();
9 | const sanitizedReq = sanitizeRequest(request);
10 |
11 | if (abortIf(oldState, sanitizedReq)) {
12 | resolve(oldState);
13 |
14 | return;
15 | }
16 |
17 | const { route, params } = resolveRouteAndParams(sanitizedReq.parsedUrl);
18 | const enterResolvedRoute = execEffect(historyEffect, sanitizedReq.url, statusCode);
19 |
20 | resolve(
21 | enterResolvedRoute ?
22 | enterRoute(sanitizedReq, route, params)(getState, patchState, dispatchAction) :
23 | null
24 | );
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/universal/app/components/router/util/createRouter.js:
--------------------------------------------------------------------------------
1 | import nanorouter from "nanorouter";
2 | import routes from "../../../routes";
3 |
4 | export default function createRouter() {
5 | const router = nanorouter({ default: "/404" });
6 | let result;
7 |
8 | Object.values(routes).forEach((route, i, arr) => {
9 | let urlPattern = route.url;
10 |
11 | if (i === arr.length - 1) {
12 | if (typeof urlPattern === "string") {
13 | // throw new Error("Expected the last catch-all route to have no url pattern");
14 | }
15 | urlPattern = "404";
16 | } else if (typeof urlPattern !== "string") {
17 | // Skip routes without an url pattern
18 | return;
19 | }
20 | router.on(urlPattern, urlParams => (result = { route, urlParams }));
21 | });
22 |
23 | return url => {
24 | router(url);
25 |
26 | return result;
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/universal/app/components/router/util/resolveRouteAndParams.js:
--------------------------------------------------------------------------------
1 | import createRouter from "./createRouter";
2 |
3 | const router = createRouter();
4 |
5 | export default function resolveRouteAndParams(parsedUrl) {
6 | const { route, urlParams } = router(parsedUrl.pathname);
7 |
8 | return {
9 | route,
10 | params: Object.assign(parsedUrl.query, urlParams),
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/universal/app/components/router/util/sanitizeRequest.js:
--------------------------------------------------------------------------------
1 | import url from "url";
2 |
3 | const defaultRequest = {
4 | method: "GET",
5 | url: "/",
6 | body: {},
7 | };
8 |
9 | function parseUrlAndQueryString(u) {
10 | return url.parse(u, true);
11 | }
12 |
13 | export default function sanitizeRequest(req) {
14 | const request = typeof req === "string" ? { ...defaultRequest, url: req } : req;
15 | const parsedUrl = parseUrlAndQueryString(request.url);
16 |
17 | return {
18 | sanitized: true,
19 | method: request.method.toUpperCase(),
20 | url: parsedUrl.path + (typeof parsedUrl.hash === "string" ? parsedUrl.hash : ""),
21 | parsedUrl,
22 | body: request.body,
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/universal/app/components/session/session.js:
--------------------------------------------------------------------------------
1 | import renderChild from "../util/renderChild";
2 | import createSession from "../../effects/api/session/create";
3 | import destroySession from "../../effects/api/session/destroy";
4 | import defineState from "../../store/defineState";
5 | import contexts from "../../contexts";
6 | import session from "../../effects/session";
7 |
8 | const name = "session";
9 |
10 | export const state = defineState({
11 | scope: name,
12 | context: contexts.state,
13 | initialState: {
14 | user: null,
15 | token: null,
16 | },
17 | hydrate(dehydrated, execEffect) {
18 | const state = execEffect(session.read);
19 |
20 | return {
21 | ...dehydrated,
22 | ...state,
23 | };
24 | },
25 | actions: {
26 | create: (name, password) => (getState, patchState, dispatchAction, execEffect) =>
27 | // No need to write the session because the API is doing that for us
28 | execEffect(createSession, name, password).then(res => {
29 | patchState({
30 | user: res.user,
31 | token: res.token,
32 | });
33 | }),
34 | destroy: () => (getState, patchState, dispatchAction, execEffect) =>
35 | // No need to write the session because the API is doing that for us
36 | execEffect(destroySession, getState().token).then(res => {
37 | patchState({
38 | user: null,
39 | token: null,
40 | });
41 | }),
42 | },
43 | });
44 |
45 | export default renderChild;
46 |
--------------------------------------------------------------------------------
/universal/app/components/store/store.js:
--------------------------------------------------------------------------------
1 | import defineComponent from "../util/defineComponent";
2 | import defineState from "../../store/defineState";
3 | import contexts from "../../contexts";
4 |
5 | const name = "store";
6 |
7 | export const state = defineState({
8 | scope: name,
9 | context: contexts.state,
10 | actions: {
11 | hydrateStates() {
12 | return (getState, patchState, dispatchAction, execEffect) => {
13 | Object.values(contexts.state.scopes).map(scope => dispatchAction(scope.hydrate()));
14 | };
15 | },
16 | },
17 | });
18 |
19 | export default defineComponent({
20 | name,
21 | getChildContext(props) {
22 | return {
23 | ...this,
24 | store: props.store,
25 | };
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/universal/app/components/top5/top5.js:
--------------------------------------------------------------------------------
1 | import defineState from "../../store/defineState";
2 | import contexts from "../../contexts";
3 | import defineComponent from "../util/defineComponent";
4 | import getTop5 from "../../effects/api/posts/getTop5";
5 | import Posts from "../posts/posts";
6 |
7 | const name = "top5";
8 |
9 | export const state = defineState({
10 | scope: name,
11 | context: contexts.state,
12 | initialState: {
13 | posts: null,
14 | },
15 | actions: {
16 | enter: () => (getState, patchState, dispatchAction, execEffect) =>
17 | execEffect(getTop5).then(posts => {
18 | patchState({
19 | posts,
20 | });
21 | }),
22 | update: () => (getState, patchState, dispatchAction, execEffect) => Function.prototype,
23 | },
24 | });
25 |
26 | export default defineComponent({
27 | name,
28 | connectToStore: {
29 | watch: [state.select],
30 | },
31 | render(props, state) {
32 | return ;
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/universal/app/components/util/attrSelector.js:
--------------------------------------------------------------------------------
1 | export default function attrSelector(cssSelector) {
2 | return `[data-${ cssSelector }]`;
3 | }
4 |
--------------------------------------------------------------------------------
/universal/app/components/util/hookIntoEvent.js:
--------------------------------------------------------------------------------
1 | export default function hookIntoEvent(eventProp, handler) {
2 | return (...args) => {
3 | const e = args[1];
4 | const props = args[2];
5 | const originalHandler = props[eventProp];
6 |
7 | handler(...args);
8 |
9 | if (typeof originalHandler === "function") {
10 | originalHandler.call(e.currentTarget, e);
11 | }
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/universal/app/components/util/renderChild.js:
--------------------------------------------------------------------------------
1 | export default function RenderChild({ children }) {
2 | return children[0];
3 | }
4 |
--------------------------------------------------------------------------------
/universal/app/components/util/withContext.js:
--------------------------------------------------------------------------------
1 | import { Component } from "preact";
2 | import renderChild from "./renderChild";
3 |
4 | export default class WithContext extends Component {
5 | constructor() {
6 | super();
7 | this.render = renderChild;
8 | }
9 | getChildContext() {
10 | return this.props.context;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/universal/app/contexts.js:
--------------------------------------------------------------------------------
1 | export default {
2 | state: {
3 | name: "app",
4 | scopes: Object.create(null),
5 | },
6 | chunkEntries: Object.create(null),
7 | };
8 |
--------------------------------------------------------------------------------
/universal/app/createApp.js:
--------------------------------------------------------------------------------
1 | import App from "./components/app/app";
2 | import createStore from "./store/createStore";
3 | import contexts from "./contexts";
4 |
5 | export default function createApp(initialState, effectContext) {
6 | const store = createStore(contexts.state, initialState, effectContext);
7 | const app = ;
8 |
9 | return {
10 | app,
11 | store,
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/universal/app/effects/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/universal/app/effects/api/api.browser.js:
--------------------------------------------------------------------------------
1 | import fetch from "unfetch";
2 |
3 | const root = "/api";
4 |
5 | export default function api() {
6 | return (url, options) => fetch(root + url, options);
7 | }
8 |
--------------------------------------------------------------------------------
/universal/app/effects/api/api.node.js:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import { parse as parseCookie } from "cookie";
3 | import config from "../../../config/server";
4 |
5 | const root = `http://${ config.hostname }:${ config.port }/api`;
6 |
7 | function includeCredentials(options) {
8 | return typeof options.credentials === "string" && options.credentials.toLowerCase() === "same-origin";
9 | }
10 |
11 | export default function api({ req, res }) {
12 | return (url, options = {}) => {
13 | if (includeCredentials(options)) {
14 | options.headers = options.headers || {};
15 | options.headers.cookie = req.headers.cookie;
16 | }
17 |
18 | return fetch(root + url, options).then(apiRes => {
19 | const cookie = apiRes.headers.get("set-cookie");
20 |
21 | if (!cookie) {
22 | return apiRes;
23 | }
24 |
25 | const oldCookie = typeof req.headers.cookie === "string" ? parseCookie(req.headers.cookie) : null;
26 | const newCookie = parseCookie(cookie);
27 |
28 | if (oldCookie === null || oldCookie[config.session.name] === newCookie[config.session.name]) {
29 | return apiRes;
30 | }
31 |
32 | // The API responded with a different session, we need to switch this one
33 | return new Promise((resolve, reject) => {
34 | req.session.destroy(err => {
35 | if (err) {
36 | reject(err);
37 |
38 | return;
39 | }
40 | res.removeHeader("Set-Cookie");
41 | res.header("Set-Cookie", cookie);
42 | resolve(apiRes);
43 | });
44 | });
45 | });
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/universal/app/effects/api/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/effects/api/index.js
--------------------------------------------------------------------------------
/universal/app/effects/api/posts/getAll.js:
--------------------------------------------------------------------------------
1 | import api from "../../api";
2 |
3 | export default function getAll(context) {
4 | return () => api(context)("/posts").then(res => res.json()).then(res => res.items);
5 | }
6 |
--------------------------------------------------------------------------------
/universal/app/effects/api/posts/getTop5.js:
--------------------------------------------------------------------------------
1 | import api from "../../api";
2 |
3 | export default function getTop5(context) {
4 | return () => api(context)("/posts?limit=5&sortBy=starred").then(res => res.json()).then(res => res.items);
5 | }
6 |
--------------------------------------------------------------------------------
/universal/app/effects/api/session/create.js:
--------------------------------------------------------------------------------
1 | import api from "../../api";
2 |
3 | const defaultOptions = {
4 | method: "POST",
5 | credentials: "same-origin",
6 | headers: {
7 | "Content-Type": "application/json",
8 | },
9 | };
10 |
11 | export default function create(context) {
12 | return (name, password) =>
13 | api(context)("/session", {
14 | ...defaultOptions,
15 | body: JSON.stringify({ name, password }),
16 | })
17 | .then(res => res.json())
18 | .then(res => {
19 | if (res.status === "success") {
20 | return res.data;
21 | }
22 |
23 | throw new Error(res.message);
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/universal/app/effects/api/session/destroy.js:
--------------------------------------------------------------------------------
1 | import api from "../../api";
2 |
3 | function getOptions(token) {
4 | return {
5 | method: "DELETE",
6 | credentials: "same-origin",
7 | headers: {
8 | "Content-Type": "application/json",
9 | Authorization: "JWT " + token,
10 | },
11 | };
12 | }
13 |
14 | export default function destroy(context) {
15 | return token =>
16 | api(context)("/session", getOptions(token)).then(res => res.json()).then(res => {
17 | if (res.status === "success") {
18 | return res.data;
19 | }
20 |
21 | throw new Error(res.message);
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/universal/app/effects/csrf/csrf.browser.js:
--------------------------------------------------------------------------------
1 | export default function csrf({ req }) {
2 | return () => null;
3 | }
4 |
--------------------------------------------------------------------------------
/universal/app/effects/csrf/csrf.node.js:
--------------------------------------------------------------------------------
1 | import has from "../../util/has";
2 |
3 | export default function csrf({ req }) {
4 | return () => {
5 | const token = has(req.session, "csrf") ? req.session.csrf : req.csrfToken();
6 |
7 | req.session.csrf = token;
8 |
9 | return token;
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/universal/app/effects/csrf/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/effects/csrf/index.js
--------------------------------------------------------------------------------
/universal/app/effects/document/document.browser.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setTitle: () => title => {
3 | document.title = title;
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/universal/app/effects/document/document.node.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setTitle: () => Function.prototype,
3 | };
4 |
--------------------------------------------------------------------------------
/universal/app/effects/document/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/effects/document/index.js
--------------------------------------------------------------------------------
/universal/app/effects/history/history.browser.js:
--------------------------------------------------------------------------------
1 | export default {
2 | push: () => url => {
3 | history.pushState(null, "", url);
4 |
5 | return true; // true = enter the next route
6 | },
7 | replace: () => url => {
8 | history.replaceState(null, "", url);
9 |
10 | return true; // true = enter the next route
11 | },
12 | reset: () => url => {
13 | try {
14 | localStorage.clear();
15 | } catch (err) {
16 | console.error(err);
17 | }
18 | try {
19 | sessionStorage.clear();
20 | } catch (err) {
21 | console.error(err);
22 | }
23 | window.location = url;
24 |
25 | return true; // true = enter the next route
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/universal/app/effects/history/history.node.js:
--------------------------------------------------------------------------------
1 | import { SEE_OTHER } from "../../util/statusCodes";
2 |
3 | export default {
4 | push: () => url => {
5 | throw new Error(`Cannot push ${ url } to server history. Use replace() to respond with a redirect.`);
6 | },
7 | replace: ({ res }) => (url, statusCode = SEE_OTHER) => {
8 | // It is important to use the redirect() method here or otherwise express-session
9 | // won't save the session on POST requests
10 | // https://stackoverflow.com/a/26532987
11 | res.redirect(statusCode, url);
12 |
13 | return false; // false = do not enter the next route
14 | },
15 | reset: ({ res }) => (url, statusCode = SEE_OTHER) => {
16 | res.redirect(statusCode, url);
17 |
18 | return false; // false = do not enter the next route
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/universal/app/effects/history/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/effects/history/index.js
--------------------------------------------------------------------------------
/universal/app/effects/session/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jhnns/spa-vs-universal/2224ebc5e24f0988c6b611a0752385e695e5fa7e/universal/app/effects/session/index.js
--------------------------------------------------------------------------------
/universal/app/effects/session/session.browser.js:
--------------------------------------------------------------------------------
1 | function returnNull() {
2 | return null;
3 | }
4 |
5 | export default {
6 | read: () => returnNull,
7 | write: () => Function.prototype,
8 | readFlash: () => returnNull,
9 | writeFlash: () => Function.prototype,
10 | };
11 |
--------------------------------------------------------------------------------
/universal/app/effects/session/session.node.js:
--------------------------------------------------------------------------------
1 | import has from "../../util/has";
2 |
3 | export default {
4 | read: ({ req }) => () => req.session,
5 | write: ({ req }) => session => {
6 | Object.assign(req.session, session);
7 | },
8 | readFlash: ({ req }) => key => {
9 | const flashes = req.session.flashes ? req.session.flashes : {};
10 |
11 | if (has(flashes, key) === false) {
12 | return null;
13 | }
14 |
15 | const value = flashes[key];
16 |
17 | delete flashes[key];
18 |
19 | return value;
20 | },
21 | writeFlash: ({ req }) => (key, value) => {
22 | const flashes = req.session.flashes ? req.session.flashes : {};
23 |
24 | flashes[key] = value;
25 | req.session.flashes = flashes;
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/universal/app/env.js:
--------------------------------------------------------------------------------
1 | const env = process.env.NODE_ENV || "development";
2 |
3 | export const isProd = env === "production";
4 | export const isDev = isProd === false;
5 |
6 | export default env;
7 |
--------------------------------------------------------------------------------
/universal/app/routes/about/about.js:
--------------------------------------------------------------------------------
1 | import { state as documentState } from "../../components/document/document";
2 | import Component, { state } from "../../components/about/about";
3 |
4 | export function GET(request, route, params) {
5 | return (dispatchAction, getState, execEffect) => {
6 | dispatchAction(
7 | documentState.actions.update({
8 | statusCode: 200,
9 | title: "About",
10 | headerTags: [],
11 | })
12 | );
13 |
14 | return state.actions;
15 | };
16 | }
17 |
18 | export default Component;
19 |
--------------------------------------------------------------------------------
/universal/app/routes/about/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../../components/chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | chunk: "about",
6 | context: contexts.chunkEntries,
7 | load: () => import("./about" /* webpackChunkName: "about" */),
8 | });
9 |
--------------------------------------------------------------------------------
/universal/app/routes/allPosts/allPosts.js:
--------------------------------------------------------------------------------
1 | import { state as documentState } from "../../components/document/document";
2 | import Component, { state } from "../../components/allPosts/allPosts";
3 |
4 | export function GET(request, route, params) {
5 | return (dispatchAction, getState, execEffect) => {
6 | dispatchAction(
7 | documentState.actions.update({
8 | statusCode: 200,
9 | title: "All Peerigon News",
10 | headerTags: [],
11 | })
12 | );
13 |
14 | return state.actions;
15 | };
16 | }
17 |
18 | export default Component;
19 |
--------------------------------------------------------------------------------
/universal/app/routes/allPosts/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../../components/chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | name: "allPosts",
6 | chunk: "posts",
7 | context: contexts.chunkEntries,
8 | load: () => import("./allPosts" /* webpackChunkName: "posts" */),
9 | });
10 |
--------------------------------------------------------------------------------
/universal/app/routes/error/error.js:
--------------------------------------------------------------------------------
1 | import { state as documentState } from "../../components/document/document";
2 | import Component, { state } from "../../components/error/error";
3 | import has from "../../util/has";
4 |
5 | const emptyArr = [];
6 |
7 | export function GET(request, route, params) {
8 | return (dispatchAction, getState, execEffect) => {
9 | const statusCode = has(params, "statusCode") ? params.statusCode : 500;
10 | const title = has(params, "title") ? params.title : "Error";
11 | const headerTags = has(params, "headerTags") ? params.headerTags : emptyArr;
12 |
13 | dispatchAction(
14 | documentState.actions.update({
15 | statusCode,
16 | title,
17 | headerTags,
18 | })
19 | );
20 |
21 | return state.actions;
22 | };
23 | }
24 |
25 | export default Component;
26 |
--------------------------------------------------------------------------------
/universal/app/routes/error/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../../components/chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | chunk: "error",
6 | context: contexts.chunkEntries,
7 | load: () => import("./error" /* webpackChunkName: "error" */),
8 | });
9 |
--------------------------------------------------------------------------------
/universal/app/routes/index.js:
--------------------------------------------------------------------------------
1 | import top5 from "./top5";
2 | import allPosts from "./allPosts";
3 | import about from "./about";
4 | import session from "./session";
5 | import error from "./error";
6 | import notFound from "./notFound";
7 | import addObjectKeys from "../util/addObjectKeys";
8 |
9 | function defineRoutes(routes) {
10 | return addObjectKeys(routes, "name");
11 | }
12 |
13 | export default defineRoutes({
14 | top5: {
15 | url: "/",
16 | entry: top5.import,
17 | placeholder: () => top5.Placeholder,
18 | },
19 | allPosts: {
20 | url: "/all",
21 | entry: allPosts.import,
22 | placeholder: () => allPosts.Placeholder,
23 | },
24 | about: {
25 | url: "/about",
26 | entry: about.import,
27 | placeholder: () => about.Placeholder,
28 | },
29 | session: {
30 | url: "/session",
31 | entry: session.import,
32 | },
33 | error: {
34 | entry: error.import,
35 | error: true,
36 | placeholder: () => error.Placeholder,
37 | },
38 | notFound: {
39 | entry: notFound.import,
40 | error: true,
41 | placeholder: () => notFound.Placeholder,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/universal/app/routes/notFound/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../../components/chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | chunk: "notFound",
6 | context: contexts.chunkEntries,
7 | load: () => import("./notFound" /* webpackChunkName: "notFound" */),
8 | });
9 |
--------------------------------------------------------------------------------
/universal/app/routes/notFound/notFound.js:
--------------------------------------------------------------------------------
1 | import { state as routerState } from "../../components/router/router";
2 | import Component from "../../components/error/error";
3 | import routes from "../../routes";
4 |
5 | export function GET(request, route, params) {
6 | return (dispatchAction, getState, execEffect) => {
7 | dispatchAction(
8 | routerState.actions.enter(request, routes.error, {
9 | statusCode: 404,
10 | title: "Not Found",
11 | message: "The requested route does not exist",
12 | })
13 | );
14 | };
15 | }
16 |
17 | export default Component;
18 |
--------------------------------------------------------------------------------
/universal/app/routes/session/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../../components/chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | chunk: "session",
6 | context: contexts.chunkEntries,
7 | load: () => import("./session" /* webpackChunkName: "session" */),
8 | });
9 |
--------------------------------------------------------------------------------
/universal/app/routes/session/session.js:
--------------------------------------------------------------------------------
1 | import { state as routerState } from "../../components/router/router";
2 | import { state as sessionState } from "../../components/session/session";
3 | import { SEE_OTHER } from "../../util/statusCodes";
4 | import contexts from "../../contexts";
5 | import routes from "../../routes";
6 |
7 | export function POST(request, route, params) {
8 | return (dispatchAction, getState, execEffect) => {
9 | function abort() {
10 | dispatchAction(formState.actions.saveInSessionFlash());
11 |
12 | return dispatchAction(routerState.actions.replace(params.previous, SEE_OTHER));
13 | }
14 |
15 | const form = params.form;
16 | const formState = contexts.state.scopes[form];
17 | const formData = request.body;
18 |
19 | dispatchAction(formState.actions.fillOut(formData));
20 |
21 | const validationResult = dispatchAction(formState.actions.validate());
22 |
23 | if (validationResult.isValid === false) {
24 | return abort();
25 | }
26 |
27 | dispatchAction(formState.actions.updateSubmitResult(null));
28 |
29 | return dispatchAction(sessionState.actions.create(formData.name, formData.password)).then(
30 | result => {
31 | dispatchAction(formState.actions.updateSubmitResult(result));
32 | dispatchAction(formState.actions.clear());
33 |
34 | return dispatchAction(routerState.actions.replace(params.next, SEE_OTHER));
35 | },
36 | result => {
37 | dispatchAction(formState.actions.updateSubmitResult(result));
38 |
39 | return abort();
40 | }
41 | );
42 | };
43 | }
44 |
45 | export function DELETE(request, route, params) {
46 | return (dispatchAction, getState, execEffect) =>
47 | dispatchAction(sessionState.actions.destroy()).then(
48 | () => dispatchAction(routerState.actions.reset(params.next)),
49 | err => dispatchAction(routerState.actions.enter(request, routes.error, err))
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/universal/app/routes/top5/index.js:
--------------------------------------------------------------------------------
1 | import defineChunkEntry from "../../components/chunks/defineChunkEntry";
2 | import contexts from "../../contexts";
3 |
4 | export default defineChunkEntry({
5 | name: "top5",
6 | chunk: "posts",
7 | context: contexts.chunkEntries,
8 | load: () => import("./top5" /* webpackChunkName: "posts" */),
9 | });
10 |
--------------------------------------------------------------------------------
/universal/app/routes/top5/top5.js:
--------------------------------------------------------------------------------
1 | import { state as documentState } from "../../components/document/document";
2 | import Component, { state } from "../../components/top5/top5";
3 |
4 | export function GET(request, route, params) {
5 | return (dispatchAction, getState, execEffect) => {
6 | dispatchAction(
7 | documentState.actions.update({
8 | statusCode: 200,
9 | title: "Top 5 Peerigon News",
10 | headerTags: [],
11 | })
12 | );
13 |
14 | return state.actions;
15 | };
16 | }
17 |
18 | export default Component;
19 |
--------------------------------------------------------------------------------
/universal/app/server/assetTags.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { isProd } from "../env";
3 | import { assetsJson as pathToAssetsJson } from "./paths";
4 |
5 | const assetTags = isProd === true ? prepareAssetTags() : null;
6 |
7 | function prepareAssetTags() {
8 | const assetsJson = JSON.parse(fs.readFileSync(pathToAssetsJson, "utf8"));
9 |
10 | return Object.keys(assetsJson).reduce((assetTags, chunkName) => {
11 | const assets = assetsJson[chunkName];
12 |
13 | assetTags[chunkName] = assets
14 | .map(asset => asset.replace(/\.gz$/, ""))
15 | .map(asset => {
16 | if (/\.js$/.test(asset) === true) {
17 | return ``;
18 | }
19 | if (/\.css$/.test(asset) === true) {
20 | return ` `;
21 | }
22 |
23 | return "";
24 | })
25 | .join("");
26 |
27 | return assetTags;
28 | }, {});
29 | }
30 |
31 | export default function get(chunkName) {
32 | const tags = assetTags === null ? prepareAssetTags() : assetTags;
33 | const tag = tags[chunkName];
34 |
35 | if (typeof tag !== "string") {
36 | throw new Error(`No asset tag for chunk ${ chunkName }`);
37 | }
38 |
39 | return tag;
40 | }
41 |
--------------------------------------------------------------------------------
/universal/app/server/createRenderStream.js:
--------------------------------------------------------------------------------
1 | import renderToString from "preact-render-to-string";
2 | import streamTemplate from "stream-template";
3 | import serializeJavascript from "serialize-javascript";
4 | import assetTags from "./assetTags";
5 |
6 | export default function createRenderStream({ title, headerTags, html, state, chunks }) {
7 | const renderedHeaderTags = Promise.resolve(headerTags).then(nodes =>
8 | nodes.map(renderToString).reduce((str, tag) => str + tag, "")
9 | );
10 | const renderedState = state.then(state => serializeJavascript(state, { isJSON: true, space: 0 }));
11 |
12 | return streamTemplate`
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ${ title }
22 | ${ renderedHeaderTags }
23 | ${ assetTags("client") }
24 | ${ chunks.then(chunkNames => chunkNames.map(assetTags).join("")) }
25 |
26 |
27 | ${ html }
28 |
31 |
32 |
33 | `;
34 | }
35 |
--------------------------------------------------------------------------------
/universal/app/server/index.js:
--------------------------------------------------------------------------------
1 | import { h } from "preact";
2 |
3 | global.h = h;
4 |
5 | export default function handleRequest(req, res) {
6 | const createApp = require("../createApp").default;
7 | const renderApp = require("./renderApp").default;
8 | const createRenderStream = require("./createRenderStream").default;
9 | const routerState = require("../components/router/router").state;
10 | const documentState = require("../components/document/document").state;
11 | const storeState = require("../components/store/store").state;
12 | const preloadAllChunkEntries = require("./preloadAllChunkEntries").default;
13 | const has = require("../util/has").default;
14 | const routes = require("../routes").default;
15 |
16 | const initialState = {};
17 | const effectContext = { req, res };
18 | const { app, store } = createApp(initialState, effectContext);
19 | const firstRouterAction = has(req, "error") ?
20 | routerState.actions.enter(req, routes.error, req.error) :
21 | routerState.actions.enter(req);
22 |
23 | res.header("Content-Type", "text/html");
24 |
25 | store.dispatch(storeState.actions.hydrateStates());
26 |
27 | const routingFinished = preloadAllChunkEntries().then(() => store.dispatch(firstRouterAction));
28 |
29 | store.when(s => documentState.select(s).statusCode).then(statusCode => {
30 | const appRendered = routingFinished.then(() => renderApp(app, store));
31 |
32 | res.statusCode = statusCode;
33 | createRenderStream({
34 | title: store.when(s => documentState.select(s).title),
35 | headerTags: store.when(s => documentState.select(s).headerTags),
36 | html: appRendered.then(({ html }) => html),
37 | state: appRendered.then(({ state }) => state),
38 | chunks: appRendered.then(({ chunks }) => chunks),
39 | }).pipe(res);
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/universal/app/server/paths.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | export const assetsJson = path.resolve(process.cwd(), "dist", "public", "assets.json");
4 |
--------------------------------------------------------------------------------
/universal/app/server/preloadAllChunkEntries.js:
--------------------------------------------------------------------------------
1 | import contexts from "../contexts";
2 |
3 | let promise = null;
4 |
5 | export default function preloadAllChunkEntries() {
6 | if (promise !== null) {
7 | return promise;
8 | }
9 |
10 | return (promise = Promise.all(Object.values(contexts.chunkEntries).map(entry => entry.load())));
11 | }
12 |
--------------------------------------------------------------------------------
/universal/app/server/renderApp.js:
--------------------------------------------------------------------------------
1 | import renderToString from "preact-render-to-string";
2 | import { selectLoadedChunks } from "../components/chunks/chunks";
3 |
4 | export default function renderApp(app, store) {
5 | return Promise.resolve().then(() => renderToString(app)).then(html => {
6 | const state = store.getState();
7 |
8 | return {
9 | html,
10 | state,
11 | chunks: selectLoadedChunks(state),
12 | };
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/universal/app/store/createReducer.js:
--------------------------------------------------------------------------------
1 | // 1 = context, 2 = scope, 3 = action, 4 = mutation
2 | const actionTypePattern = /^(\w+)\/(\w+)\/(\w+)\/(\w+)$/i;
3 |
4 | export default function createReducer(stateContext) {
5 | return function (state = {}, action) {
6 | const typeMatch = actionTypePattern.exec(action.type);
7 |
8 | if (typeMatch !== null) {
9 | const scope = typeMatch[2];
10 | const updateType = typeMatch[4];
11 |
12 | switch (updateType) {
13 | case "patch":
14 | return {
15 | ...state,
16 | [scope]: {
17 | ...stateContext.scopes[scope].select(state),
18 | ...action.payload,
19 | },
20 | };
21 | case "put":
22 | return {
23 | ...state,
24 | [scope]: action.payload,
25 | };
26 | }
27 | }
28 |
29 | return state;
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/universal/app/store/createStore.js:
--------------------------------------------------------------------------------
1 | import { createStore as reduxCreateStore, compose, applyMiddleware } from "redux";
2 | import effectMiddleware from "./effectMiddleware";
3 | import thunkMiddleware from "./thunkMiddleware";
4 | import createReducer from "./createReducer";
5 | import enhanceStore from "./enhanceStore";
6 |
7 | const useReduxDevTools = process.env.NODE_ENV === "development" && typeof devToolsExtension === "function"; // eslint-disable-line no-undef
8 |
9 | export default function createStore(stateContext, initialState, effectContext) {
10 | return reduxCreateStore(
11 | createReducer(stateContext),
12 | initialState,
13 | compose(
14 | applyMiddleware(thunkMiddleware(), effectMiddleware((effect, args) => effect(effectContext)(...args))),
15 | enhanceStore(stateContext),
16 | // Use redux devtools when installed in the browser
17 | // @see https://github.com/zalmoxisus/redux-devtools-extension#implementation
18 | useReduxDevTools ? devToolsExtension() : f => f // eslint-disable-line no-undef
19 | )
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/universal/app/store/defineState.js:
--------------------------------------------------------------------------------
1 | import has from "../util/has";
2 |
3 | const emptyObj = {};
4 |
5 | function returnThis() {
6 | return this; // eslint-disable-line no-invalid-this
7 | }
8 |
9 | function isDehydratable(state) {
10 | return typeof state.toJSON === "function";
11 | }
12 |
13 | export default function defineState(descriptor) {
14 | const scope = descriptor.scope;
15 | const context = descriptor.context;
16 | const namespace = context.name + "/" + scope;
17 | const initialState = has(descriptor, "initialState") ? descriptor.initialState : emptyObj;
18 | const hydrate = descriptor.hydrate;
19 |
20 | function selectState(contextState) {
21 | return has(contextState, scope) ? contextState[scope] : initialState;
22 | }
23 |
24 | function isHydrated(state) {
25 | return hydrate === undefined || (state !== initialState && typeof state.toJSON === "function");
26 | }
27 |
28 | if (typeof scope !== "string") {
29 | throw new Error("Scope is missing");
30 | }
31 | if (context === undefined) {
32 | throw new Error("State context is missing");
33 | }
34 | if (has(context.scopes, scope)) {
35 | throw new Error(`Scope ${ scope } is already defined on given state context`);
36 | }
37 |
38 | const actionDescriptor = has(descriptor, "actions") ? descriptor.actions : emptyObj;
39 | const state = {
40 | context,
41 | namespace,
42 | scope,
43 | actions: Object.keys(actionDescriptor).reduce((actions, actionName) => {
44 | const execute = actionDescriptor[actionName];
45 | const type = namespace + "/" + actionName;
46 |
47 | actions[actionName] = (...args) => (dispatchAction, getState, execEffect) => {
48 | function getScopedState() {
49 | return selectState(getState());
50 | }
51 |
52 | function patchState(patch) {
53 | return dispatchAction({
54 | type: type + "/patch",
55 | payload: patch,
56 | });
57 | }
58 |
59 | return execute(...args)(getScopedState, patchState, dispatchAction, execEffect);
60 | };
61 |
62 | return actions;
63 | }, {}),
64 | hydrate() {
65 | return (dispatchAction, getState, execEffect) => {
66 | const dehydrated = selectState(getState());
67 |
68 | if (isHydrated(dehydrated)) {
69 | return;
70 | }
71 |
72 | const hydrated = hydrate(dehydrated, execEffect);
73 |
74 | if (isDehydratable(hydrated) === false) {
75 | hydrated.toJSON = returnThis;
76 | }
77 |
78 | dispatchAction({
79 | type: namespace + "/hydrate/put",
80 | payload: hydrated,
81 | });
82 | };
83 | },
84 | select: selectState,
85 | };
86 |
87 | context.scopes[scope] = state;
88 |
89 | return state;
90 | }
91 |
--------------------------------------------------------------------------------
/universal/app/store/effectMiddleware.js:
--------------------------------------------------------------------------------
1 | import has from "../util/has";
2 |
3 | export function createEffectAction(effect, args) {
4 | return {
5 | type: "effect",
6 | payload: {
7 | effect,
8 | args,
9 | },
10 | };
11 | }
12 |
13 | export default function effectMiddleware(execEffect) {
14 | return store => next => action => {
15 | if (has(action, "payload")) {
16 | const payload = action.payload;
17 |
18 | if (has(payload, "effect")) {
19 | return execEffect(payload.effect, payload.args);
20 | }
21 | }
22 |
23 | return next(action);
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/universal/app/store/enhanceStore.js:
--------------------------------------------------------------------------------
1 | function isDefined(result) {
2 | return result !== null && result !== undefined;
3 | }
4 |
5 | export default function enhanceStore(stateContext) {
6 | return createStore => (reducers, initialState, enhancers) => {
7 | const store = createStore(reducers, initialState, enhancers);
8 | const enhancedStore = {
9 | ...store,
10 | context: stateContext,
11 | watch(select, onChange) {
12 | let oldValue = select(this.getState());
13 |
14 | return this.subscribe(() => {
15 | const newValue = select(this.getState());
16 |
17 | if (oldValue !== newValue) {
18 | onChange(newValue, oldValue);
19 | oldValue = newValue;
20 | }
21 | });
22 | },
23 | when(select, condition = isDefined) {
24 | return new Promise((resolve, reject) => {
25 | let unsubscribe = Function.prototype;
26 |
27 | function check(value) {
28 | const result = condition(value);
29 |
30 | if (result === true) {
31 | unsubscribe();
32 | resolve(value);
33 | }
34 |
35 | return result;
36 | }
37 |
38 | if (check(select(this.getState())) === false) {
39 | unsubscribe = this.watch(select, check);
40 | }
41 | });
42 | },
43 | };
44 |
45 | return enhancedStore;
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/universal/app/store/thunkMiddleware.js:
--------------------------------------------------------------------------------
1 | import { createEffectAction } from "./effectMiddleware";
2 |
3 | function executeAction(action, dispatchAction, getState) {
4 | function execEffect(effect, ...args) {
5 | return dispatchAction(createEffectAction(effect, args));
6 | }
7 |
8 | return action(dispatchAction, getState, execEffect);
9 | }
10 |
11 | export default function thunkMiddleware() {
12 | return ({ dispatch, getState }) => next => action => {
13 | if (typeof action === "function") {
14 | return executeAction(action, dispatch, getState);
15 | }
16 |
17 | return next(action);
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/universal/app/styles/a11y.js:
--------------------------------------------------------------------------------
1 | export const offscreen = {
2 | position: "absolute",
3 | left: -10000,
4 | top: "auto",
5 | width: 1,
6 | height: 1,
7 | overflow: "hidden",
8 | };
9 |
--------------------------------------------------------------------------------
/universal/app/styles/block/inputSubmit.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import { rem } from "../../styles/scales";
3 | import { mint } from "../../styles/colors";
4 | import { regularFontSize, regularLineHeight } from "../../styles/typoSizes";
5 | import nexaXBold from "../../styles/type/nexaXBold";
6 | import { regular } from "../borders";
7 | import { repeatingLinear } from "../gradients";
8 |
9 | const stripeColor = "rgba(0, 0, 0, 0.1)";
10 | const stripeAnimation = css.keyframes({
11 | from: {
12 | backgroundPosition: "0 0",
13 | },
14 | to: {
15 | backgroundPosition: "71px 0px",
16 | },
17 | });
18 | // Mobile safari adds weird styles
19 | const mobileSafariStyleFixes = {
20 | WebkitAppearance: "none",
21 | borderRadius: 0,
22 | };
23 |
24 | export default {
25 | ...nexaXBold,
26 | ...mobileSafariStyleFixes,
27 | width: "100%",
28 | fontSize: regularFontSize + "rem",
29 | lineHeight: regularLineHeight + "rem",
30 | padding: rem(7) + "rem 0",
31 | border: "none",
32 | outline: "none",
33 | backgroundColor: mint(),
34 | boxShadow: "0 0px 0px rgba(0, 0, 0, 0.3)",
35 | transition: "box-shadow 0.1s ease-in-out",
36 | ":hover": {
37 | cursor: "pointer",
38 | boxShadow: "3px 3px 3px rgba(0, 0, 0, 0.2), -3px 3px 3px rgba(0, 0, 0, 0.2)",
39 | },
40 | ":focus": {
41 | outline: regular(),
42 | },
43 | "[data-pending]": {
44 | backgroundImage: repeatingLinear("-45deg", [
45 | "transparent 0",
46 | "transparent 25px",
47 | stripeColor + " 25px",
48 | stripeColor + " 50px",
49 | ]),
50 | backgroundSize: "71px 50px",
51 | animation: stripeAnimation + " 2s linear infinite",
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/universal/app/styles/block/inputText.js:
--------------------------------------------------------------------------------
1 | import { rem } from "../../styles/scales";
2 | import { mint, red } from "../../styles/colors";
3 | import { regularFontSize, regularLineHeight } from "../../styles/typoSizes";
4 | import latoLight from "../../styles/type/latoLight";
5 | import { regular } from "../borders";
6 |
7 | const mobileSafariStyleFixes = {
8 | WebkitAppearance: "none",
9 | borderRadius: 0,
10 | };
11 |
12 | export default {
13 | ...latoLight,
14 | ...mobileSafariStyleFixes,
15 | width: "100%",
16 | fontSize: regularFontSize + "rem",
17 | lineHeight: regularLineHeight + "rem",
18 | padding: rem(7) + "rem 0",
19 | border: "none",
20 | borderBottom: regular(),
21 | outline: "none",
22 | ":focus": {
23 | borderColor: mint(),
24 | outlineColor: mint(),
25 | },
26 | "[invalid]": {
27 | borderBottomColor: red(),
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/universal/app/styles/block/sheet.js:
--------------------------------------------------------------------------------
1 | import { px } from "../../styles/scales";
2 | import { white, black } from "../../styles/colors";
3 |
4 | export const sheetPadding = px(13);
5 |
6 | export default {
7 | color: black(),
8 | backgroundColor: white(),
9 | padding: sheetPadding,
10 | };
11 |
--------------------------------------------------------------------------------
/universal/app/styles/borders.js:
--------------------------------------------------------------------------------
1 | import { black } from "./colors";
2 |
3 | export const defaultColor = black();
4 | export const regularWidth = 2;
5 | export const strongWidth = 4;
6 |
7 | export function regular(color = defaultColor) {
8 | return `${ regularWidth }px solid ${ color }`;
9 | }
10 |
11 | export function strong(color = defaultColor) {
12 | return `${ strongWidth }px solid ${ color }`;
13 | }
14 |
--------------------------------------------------------------------------------
/universal/app/styles/calc.js:
--------------------------------------------------------------------------------
1 | export default function (...bits) {
2 | return `calc(${ bits.join("") })`;
3 | }
4 |
--------------------------------------------------------------------------------
/universal/app/styles/colors.js:
--------------------------------------------------------------------------------
1 | import { color, lightness } from "kewler";
2 |
3 | export const mint = color("#46e1c8");
4 | export const mintLight35 = mint(lightness(35));
5 | export const white = color("#fff");
6 | export const silver = color("#e6e1de");
7 | export const silverLight10 = silver(lightness(10));
8 | export const black = color("#282828");
9 | export const blackLight30 = black(lightness(40));
10 | export const red = color("#f14936");
11 |
--------------------------------------------------------------------------------
/universal/app/styles/gradients.js:
--------------------------------------------------------------------------------
1 | export function linear(dir, colorStops) {
2 | return `linear-gradient(${ dir }, ${ colorStops.join(", ") })`;
3 | }
4 |
5 | export function repeatingLinear(dir, colorStops) {
6 | return `repeating-linear-gradient(${ dir }, ${ colorStops.join(", ") })`;
7 | }
8 |
--------------------------------------------------------------------------------
/universal/app/styles/layout.js:
--------------------------------------------------------------------------------
1 | import { rem } from "./scales";
2 |
3 | export const maxContentWidth = rem(34);
4 |
--------------------------------------------------------------------------------
/universal/app/styles/paddings.js:
--------------------------------------------------------------------------------
1 | import { px } from "./scales";
2 |
3 | export const paddingRegular = px(14);
4 | export const paddingBigger = px(15);
5 |
--------------------------------------------------------------------------------
/universal/app/styles/reset.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 |
3 | css.global("*", {
4 | margin: 0,
5 | padding: 0,
6 | });
7 |
8 | css.global("input", {
9 | border: "none",
10 | background: "none",
11 | });
12 |
--------------------------------------------------------------------------------
/universal/app/styles/scales.js:
--------------------------------------------------------------------------------
1 | const pxBase = 2;
2 | const pxRatio = 6 / 5;
3 | const remBase = 2 / 16;
4 | const remRatio = 6 / 5;
5 | const regularDevicePixelRatio = 2;
6 |
7 | export function px(factor) {
8 | const size = Math.pow(pxRatio, factor) * pxBase;
9 |
10 | // Round to whole device pixels
11 | return Math.floor(size * regularDevicePixelRatio) / regularDevicePixelRatio;
12 | }
13 |
14 | export function rem(factor) {
15 | return Math.pow(remRatio, factor) * remBase;
16 | }
17 |
--------------------------------------------------------------------------------
/universal/app/styles/timing.js:
--------------------------------------------------------------------------------
1 | export function msToSeconds(ms) {
2 | return ms / 1000;
3 | }
4 |
--------------------------------------------------------------------------------
/universal/app/styles/type/latoLight.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import woff2 from "../../assets/fonts/latoLatinLight.woff2";
3 | import woff from "../../assets/fonts/latoLatinLight.woff";
4 | import ttf from "../../assets/fonts/latoLatinLight.ttf";
5 |
6 | const fontStyles = {
7 | fontWeight: 200,
8 | fontStyle: "normal",
9 | };
10 |
11 | const fontFamily = css.fontFace({
12 | fontFamily: "Lato",
13 | ...fontStyles,
14 | src: `local("Lato Light"),
15 | url("${ woff2 }") format("woff2"),
16 | url("${ woff }") format("woff"),
17 | url("${ ttf }") format("truetype")`,
18 | });
19 |
20 | export default {
21 | fontFamily,
22 | ...fontStyles,
23 | };
24 |
25 |
--------------------------------------------------------------------------------
/universal/app/styles/type/nexaHeavy.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import woff2 from "../../assets/fonts/nexaHeavyWebfont.woff2";
3 | import woff from "../../assets/fonts/nexaHeavyWebfont.woff";
4 | import ttf from "../../assets/fonts/nexaHeavyWebfont.ttf";
5 |
6 | const fontStyles = {
7 | fontWeight: 900,
8 | fontStyle: "normal",
9 | };
10 |
11 | const fontFamily = css.fontFace({
12 | fontFamily: "Nexa",
13 | ...fontStyles,
14 | src: `local("Nexa Heavy"),
15 | url("${ woff2 }") format("woff2"),
16 | url("${ woff }") format("woff"),
17 | url("${ ttf }") format("truetype")`,
18 | });
19 |
20 | export default {
21 | fontFamily,
22 | ...fontStyles,
23 | };
24 |
--------------------------------------------------------------------------------
/universal/app/styles/type/nexaXBold.js:
--------------------------------------------------------------------------------
1 | import { css } from "glamor";
2 | import woff2 from "../../assets/fonts/nexaExtraboldWebfont.woff2";
3 | import woff from "../../assets/fonts/nexaExtraboldWebfont.woff";
4 | import ttf from "../../assets/fonts/nexaExtraboldWebfont.ttf";
5 |
6 | const fontStyles = {
7 | fontWeight: 700,
8 | fontStyle: "normal",
9 | };
10 |
11 | const fontFamily = css.fontFace({
12 | fontFamily: "Nexa",
13 | ...fontStyles,
14 | src: `local("Nexa XBold"),
15 | url("${ woff2 }") format("woff2"),
16 | url("${ woff }") format("woff"),
17 | url("${ ttf }") format("truetype")`,
18 | });
19 |
20 | export default {
21 | fontFamily,
22 | ...fontStyles,
23 | };
24 |
--------------------------------------------------------------------------------
/universal/app/styles/typoSizes.js:
--------------------------------------------------------------------------------
1 | import { rem } from "./scales";
2 |
3 | export const regularFontSize = rem(12);
4 | export const regularLineHeight = rem(14);
5 | export const regularMaxWidth = rem(32);
6 |
7 | export const headlineFontSize = rem(14);
8 | export const headlineLineHeight = rem(15);
9 | export const headlineMaxWidth = rem(30);
10 |
11 | export const asideFontSize = rem(11);
12 | export const asideLineHeight = rem(12);
13 | export const asideMaxWidth = rem(30);
14 |
--------------------------------------------------------------------------------
/universal/app/styles/zIndex.js:
--------------------------------------------------------------------------------
1 | export const header = 2;
2 | export const modal = 3;
3 | export const backdrop = 1;
4 |
--------------------------------------------------------------------------------
/universal/app/util/addObjectKeys.js:
--------------------------------------------------------------------------------
1 | export default function addObjectKeys(obj, propName) {
2 | Object.keys(obj).forEach(key => {
3 | obj[key][propName] = key;
4 | });
5 |
6 | return obj;
7 | }
8 |
--------------------------------------------------------------------------------
/universal/app/util/filterProps.js:
--------------------------------------------------------------------------------
1 | export default function filterProps(allProps, blacklist) {
2 | const filteredProps = {};
3 |
4 | Object.keys(allProps)
5 | .filter(key => blacklist.indexOf(key) === -1)
6 | .forEach(key => (filteredProps[key] = allProps[key]));
7 |
8 | return filteredProps;
9 | }
10 |
--------------------------------------------------------------------------------
/universal/app/util/has.js:
--------------------------------------------------------------------------------
1 | const hasOwnProperty = Object.prototype.hasOwnProperty;
2 |
3 | export default function has(obj, key) {
4 | return hasOwnProperty.call(obj, key);
5 | }
6 |
--------------------------------------------------------------------------------
/universal/app/util/htmlEntities.js:
--------------------------------------------------------------------------------
1 | export const nbsp = "\u00a0";
2 |
--------------------------------------------------------------------------------
/universal/app/util/renderUrl.js:
--------------------------------------------------------------------------------
1 | import querystring from "querystring";
2 |
3 | export default function renderUrl(urlPattern, params) {
4 | if (params === null || params === undefined) {
5 | return urlPattern;
6 | }
7 |
8 | let url = typeof urlPattern === "string" ? urlPattern : "";
9 | const searchParams = {};
10 |
11 | Object.keys(params).forEach(key => {
12 | const pattern = ":" + key;
13 | const patternIdx = url.indexOf(pattern);
14 |
15 | if (patternIdx === -1) {
16 | searchParams[key] = params[key];
17 | } else {
18 | url = url.slice(0, patternIdx - 1) + params[key] + url.slice(patternIdx + pattern.length);
19 | }
20 | });
21 |
22 | const paramString = querystring.stringify(searchParams);
23 |
24 | if (paramString === "") {
25 | return url;
26 | }
27 |
28 | return url + "?" + paramString;
29 | }
30 |
--------------------------------------------------------------------------------
/universal/app/util/statusCodes.js:
--------------------------------------------------------------------------------
1 | export const MOVED_PERMANENTLY = 301;
2 | export const FOUND = 302;
3 | export const SEE_OTHER = 303;
4 | export const TEMPORARY_REDIRECT = 307;
5 | export const PERMANENT_REDIRECT = 308;
6 |
7 | const redirectStatusCodes = [MOVED_PERMANENTLY, FOUND, SEE_OTHER, TEMPORARY_REDIRECT, PERMANENT_REDIRECT];
8 |
9 | export function isRedirect(statusCode) {
10 | return redirectStatusCodes.some(code => code === statusCode);
11 | }
12 |
--------------------------------------------------------------------------------
/universal/config/server.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 3000,
3 | "hostname": "localhost",
4 | "devServerPort": 8080,
5 | "bodyLimit": "100kb",
6 | "corsHeaders": ["Link"],
7 | "responseDelay": 300,
8 | "session": {
9 | "name": "app.id",
10 | "cookie": {
11 | "maxAge": 2592000000
12 | },
13 | "resave": false,
14 | "rolling": true,
15 | "saveUninitialized": false,
16 | "secret": "Universal JavaScript!"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/universal/server/config.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | const pathToConfig = path.resolve(process.cwd(), "config", "server");
4 | const config = require(pathToConfig);
5 |
6 | config.hostname = config.hostname || "localhost";
7 | config.port = process.env.PORT || config.port;
8 |
9 | export default config;
10 |
--------------------------------------------------------------------------------
/universal/server/dummyData/generate.js:
--------------------------------------------------------------------------------
1 | import filledArray from "filled-array";
2 | import faker from "faker";
3 |
4 | function posts() {
5 | return filledArray(
6 | i => ({
7 | id: faker.random.uuid(),
8 | title: faker.lorem.sentence(),
9 | content: faker.lorem.paragraphs(),
10 | author: faker.name.findName(),
11 | published: faker.date.past(),
12 | starred: Math.floor(Math.random() * 100),
13 | image: `/postImage${ i % 4 + 1 }.jpg`,
14 | }),
15 | 30
16 | );
17 | }
18 |
19 | posts();
20 |
--------------------------------------------------------------------------------
/universal/server/dummyData/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "353e4bdf-7436-41c1-a25c-25f0667692d9",
4 | "name": "jhnns",
5 | "image": "userImage.jpg"
6 | }
7 | ]
--------------------------------------------------------------------------------
/universal/server/env.js:
--------------------------------------------------------------------------------
1 | const env = process.env.NODE_ENV || "development";
2 |
3 | export const isProd = env === "production";
4 | export const isDev = isProd === false;
5 |
6 | export default env;
7 |
--------------------------------------------------------------------------------
/universal/tools/webpack/ResolveEffectPlugin.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | const pathToEffects = path.resolve(__dirname, "..", "..", "app", "effects");
4 |
5 | class ResolverPlugin {
6 | constructor(options) {
7 | this.options = options;
8 | }
9 | rewritePath([all, effectName]) {
10 | return path.resolve(pathToEffects, effectName, effectName + "." + this.options.target);
11 | }
12 | apply(resolver) {
13 | resolver.plugin("described-resolve", (request, callback) => {
14 | const requestPath = request.__innerRequest;
15 | const pathMatch = /[/\\]effects[/\\]([^/\\]+)$/.exec(requestPath);
16 |
17 | if (pathMatch === null) {
18 | callback();
19 |
20 | return;
21 | }
22 |
23 | resolver.doResolve(
24 | "resolve",
25 | Object.assign({}, request, {
26 | request: this.rewritePath(pathMatch),
27 | }),
28 | "resolved effect",
29 | callback
30 | );
31 | });
32 | }
33 | }
34 |
35 | export default class ResolveEffectPlugin {
36 | constructor(options) {
37 | if (options.target === undefined) {
38 | throw new Error("No target given");
39 | }
40 | this.options = options;
41 | }
42 |
43 | apply(compiler) {
44 | compiler.plugin("after-resolvers", () => {
45 | compiler.resolvers.normal.apply(new ResolverPlugin(this.options));
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/universal/tools/webpack/WriteAssetsJsonPlugin.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { assetsJson as pathToAssetJson } from "../../app/server/paths";
3 |
4 | export default class WriteAssetsJsonPlugin {
5 | apply(compiler) {
6 | compiler.plugin("after-emit", (compilation, done) => {
7 | const publicPath = compiler.options.output.publicPath || "/";
8 | const stats = compilation.getStats().toJson();
9 | const assets = stats.chunks.reduce((assets, chunk) => {
10 | assets[chunk.names[0]] = chunk.files.map(file => publicPath + file);
11 |
12 | return assets;
13 | }, {});
14 |
15 | fs.writeFile(pathToAssetJson, JSON.stringify(assets), done);
16 | });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/universal/tools/webpack/exportCssLoader.js:
--------------------------------------------------------------------------------
1 | const exportCss = `module.exports = (({ renderStatic }, oldExports) => {
2 | const oldKeys = Object.keys(oldExports);
3 | const locals = {};
4 | const { css } = renderStatic(() => {
5 | oldKeys.forEach(key => {
6 | const exportValue = oldExports[key];
7 |
8 | if (exportValue !== undefined && exportValue !== null) {
9 | locals[key] = exportValue;
10 | }
11 | });
12 |
13 | return "";
14 | });
15 | const newExports = [[module.id, css]];
16 |
17 | Object.assign(newExports, oldExports);
18 | newExports.locals = locals;
19 |
20 | return newExports;
21 | })(require("glamor-server"), module.exports);`;
22 |
23 | module.exports = function (source, sourceMaps) {
24 | this.callback(null, source + ";" + exportCss, sourceMaps);
25 | };
26 |
--------------------------------------------------------------------------------