├── static ├── favicon.ico ├── mstile-70x70.png ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon-precomposed.png ├── browserconfig.xml ├── favicon.svg ├── manifest.json └── safari-pinned-tab.svg ├── .stylelintrc ├── .prettierrc ├── flow-typed ├── hotModule.js.flow └── npm │ ├── redux-observable_v0.14.x.js │ ├── redux_v3.x.x.js │ └── rxjs_v5.0.x.js ├── src ├── components │ ├── Loading │ │ └── Loading.js │ ├── TypicalForm │ │ ├── TypicalValidation.js │ │ └── TypicalForm.js │ ├── Menu │ │ ├── Menu.scss │ │ └── Menu.js │ ├── RouterStatus │ │ ├── Status.js │ │ └── RedirectWithStatus.js │ ├── TextField │ │ └── TextField.js │ ├── SelectField │ │ └── SelectField.js │ └── GithubButton │ │ └── GithubButton.js ├── containers │ ├── Home │ │ ├── index.js │ │ └── Home.js │ ├── Examples │ │ ├── index.js │ │ └── Examples.js │ ├── NotFound │ │ ├── index.js │ │ └── NotFound.js │ ├── App │ │ ├── App.scss │ │ └── App.js │ └── Hero │ │ ├── github.svg │ │ ├── Hero.scss │ │ ├── Hero.js │ │ └── logo.svg ├── redux │ ├── neo │ │ ├── types.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── actions.js │ ├── reducers.js │ └── configureStore.js ├── helpers │ ├── Api.js │ └── Html.js ├── server │ ├── index.js │ └── render.js ├── client.js ├── config.js ├── sw.js └── utils │ └── validation.js ├── .sample.env ├── .editorconfig ├── .codeclimate.yml ├── .flowconfig ├── bin ├── server.prod.js └── server.dev.js ├── LICENCE ├── .babelrc ├── .eslintrc ├── .gitignore ├── webpack ├── server.prod.js ├── client.prod.js ├── client.dev.js └── server.dev.js ├── .circleci └── config.yml ├── package.json ├── CONTRIBUTING.md └── README.md /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-css-modules"] 3 | } 4 | -------------------------------------------------------------------------------- /static/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/mstile-70x70.png -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/mstile-144x144.png -------------------------------------------------------------------------------- /static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/mstile-150x150.png -------------------------------------------------------------------------------- /static/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/mstile-310x150.png -------------------------------------------------------------------------------- /static/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/mstile-310x310.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madeagency/reactivity/HEAD/static/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "semi": false, 6 | "parser": "flow" 7 | } 8 | -------------------------------------------------------------------------------- /flow-typed/hotModule.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var module: { 4 | hot: { 5 | accept(path: string, callback: () => void): void 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | 5 | const Loading = () =>

Loading!

6 | 7 | export default Loading 8 | -------------------------------------------------------------------------------- /.sample.env: -------------------------------------------------------------------------------- 1 | APP_HOST=http://localhost 2 | APP_PORT=3000 3 | API_URL=https://api.nasa.gov/neo/rest/v1 4 | ENABLE_SW=true 5 | API_KEY=DEMO_KEY 6 | # NODE_ENV=production 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /src/containers/Home/index.js: -------------------------------------------------------------------------------- 1 | import universal from 'react-universal-component' 2 | import Loading from 'components/Loading/Loading' 3 | 4 | export default universal(() => import('../Home/Home'), { 5 | loading: Loading 6 | }) 7 | -------------------------------------------------------------------------------- /src/containers/Examples/index.js: -------------------------------------------------------------------------------- 1 | import universal from 'react-universal-component' 2 | import Loading from 'components/Loading/Loading' 3 | 4 | export default universal(() => import('../Examples/Examples'), { 5 | loading: Loading 6 | }) 7 | -------------------------------------------------------------------------------- /src/containers/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import universal from 'react-universal-component' 2 | import Loading from 'components/Loading/Loading' 3 | 4 | export default universal(() => import('../NotFound/NotFound'), { 5 | loading: Loading 6 | }) 7 | -------------------------------------------------------------------------------- /src/containers/App/App.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font-family: Arial, Helvetica, sans-serif; 6 | } 7 | 8 | a { 9 | color: darken(#61dafb, 40); 10 | } 11 | } 12 | 13 | .container { 14 | max-width: 80%; 15 | margin: 40px auto; 16 | } 17 | -------------------------------------------------------------------------------- /src/redux/neo/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const FETCHING_DATA = 'nasa/neo/FETCHING_DATA' 4 | export const FETCHING_DATA_SUCCESS = 'nasa/neo/FETCHING_DATA_SUCCESS' 5 | export const FETCHING_DATA_FAILURE = 'nasa/neo/FETCHING_DATA_FAILURE' 6 | 7 | export type Neo = { neo_reference_id: number | string, name: string } 8 | -------------------------------------------------------------------------------- /src/components/TypicalForm/TypicalValidation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import memoize from 'lru-memoize' 4 | import { createValidator, required, email } from '../../utils/validation' 5 | 6 | const typicalValidation = createValidator({ 7 | firstName: required, 8 | email: [required, email] 9 | }) 10 | 11 | export default memoize(10)(typicalValidation) 12 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | border-bottom: 1px solid #eee; 3 | display: block; 4 | padding: 15px 10%; 5 | } 6 | 7 | .link { 8 | margin-right: 10px; 9 | font-size: 16px; 10 | color: #000; 11 | text-decoration: underline; 12 | } 13 | 14 | .activeLink { 15 | composes: link; 16 | pointer-events: none; 17 | text-decoration: none; 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux' 4 | import { combineEpics } from 'redux-observable' 5 | import { reducer as formReducer } from 'redux-form' 6 | import neo, { fetchNeoFeedEpic } from './neo' 7 | 8 | export const rootEpic = combineEpics(fetchNeoFeedEpic) 9 | 10 | export default combineReducers({ 11 | neo, 12 | form: formReducer 13 | }) 14 | -------------------------------------------------------------------------------- /src/redux/neo/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import reducer from './reducer' 4 | 5 | export { 6 | fetchData, 7 | getDataSuccess, 8 | getDataFailure, 9 | fetchNeoFeedEpic 10 | } from './actions' 11 | 12 | export { 13 | FETCHING_DATA, 14 | FETCHING_DATA_FAILURE, 15 | FETCHING_DATA_SUCCESS 16 | } from './types' 17 | 18 | export type { Neo } from './types' 19 | 20 | export default reducer 21 | -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #000000 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | javascript: 8 | mass_threshold: 60 9 | eslint: 10 | enabled: true 11 | checks: 12 | import/no-unresolved: 13 | enabled: false 14 | fixme: 15 | enabled: true 16 | ratings: 17 | paths: 18 | - "src/**/*" 19 | exclude_paths: 20 | - "webpack/*" 21 | - "node_modules/**/*" 22 | - "build/**/*" 23 | - "bin/*" 24 | - "flow-typed/**/*" 25 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import Helmet from 'react-helmet' 5 | import Status from 'components/RouterStatus/Status' 6 | 7 | const NotFound = () => ( 8 | 9 |
10 | 14 |

This Page is no longer with us.

15 |

Its in a better place now...

16 |
17 |
18 | ) 19 | 20 | export default NotFound 21 | -------------------------------------------------------------------------------- /src/components/RouterStatus/Status.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import type { Node } from 'react' 5 | import Route from 'react-router-dom/Route' 6 | 7 | type Props = { 8 | code: number, 9 | children: Node 10 | } 11 | 12 | const Status = ({ code, children }: Props) => ( 13 | { 15 | if (staticContext) { 16 | staticContext.status = code // eslint-disable-line no-param-reassign 17 | } 18 | return children 19 | }} 20 | /> 21 | ) 22 | 23 | export default Status 24 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/flow-bin/.* 3 | .*/node_modules/react-universal-component/.* 4 | .*/node_modules/serviceworker-webpack-plugin/.* 5 | .*/node_modules/webpack-flush-chunks/.* 6 | .*/node_modules/stylelint/.* 7 | .*/build/.* 8 | 9 | [include] 10 | ./flow-typed/ 11 | 12 | [libs] 13 | 14 | [lints] 15 | 16 | [options] 17 | module.name_mapper='.*\(.s?css\)' -> 'empty/object' 18 | module.name_mapper='^components' ->'/src/components' 19 | module.name_mapper='^reducers' ->'/src/redux' 20 | 21 | [version] 22 | ^0.80.0 23 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reactivity", 3 | "short_name": "Reactivity", 4 | "start_url": "./", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "theme_color": "#ffffff", 18 | "background_color": "#ffffff", 19 | "display": "standalone" 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import NavLink from 'react-router-dom/NavLink' 5 | import style from './Menu.scss' 6 | 7 | const Menu = () => ( 8 | 26 | ) 27 | 28 | export default Menu 29 | -------------------------------------------------------------------------------- /src/components/RouterStatus/RedirectWithStatus.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import Route from 'react-router-dom/Route' 5 | import Redirect from 'react-router-dom/Redirect' 6 | 7 | type Props = { 8 | from: string, 9 | to: string, 10 | status: number 11 | } 12 | 13 | const RedirectWithStatus = (props: Props) => ( 14 | { 16 | if (staticContext) { 17 | staticContext.status = props.status // eslint-disable-line no-param-reassign 18 | } 19 | return 20 | }} 21 | /> 22 | ) 23 | 24 | export default RedirectWithStatus 25 | -------------------------------------------------------------------------------- /src/components/TextField/TextField.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import type { FieldProps } from 'redux-form' 5 | 6 | type Props = { 7 | label: string, 8 | type: string 9 | } & FieldProps 10 | 11 | const TextField = ({ 12 | input, 13 | label, 14 | type, 15 | meta: { touched, error, warning } 16 | }: Props) => ( 17 |
18 | 25 |
26 | ) 27 | 28 | export default TextField 29 | -------------------------------------------------------------------------------- /bin/server.prod.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const express = require('express') 3 | const compression = require('compression') 4 | const zlib = require('zlib') 5 | const { app } = require('../src/server') 6 | const clientConfig = require('../webpack/client.prod') 7 | 8 | const { 9 | output: { publicPath } 10 | } = clientConfig 11 | const outputPath = clientConfig.output.path 12 | 13 | const clientStats = require('../build/stats.json') // eslint-disable-line 14 | const serverRender = require('../build/server.js').default 15 | 16 | app.use(publicPath, express.static(outputPath)) 17 | app.use( 18 | compression({ flush: zlib.constants.Z_PARTIAL_FLUSH }), 19 | serverRender({ clientStats }) 20 | ) 21 | 22 | app.listen(process.env.APP_PORT, () => { 23 | console.log(`Listening @ http://localhost:${process.env.APP_PORT}/`) 24 | }) 25 | -------------------------------------------------------------------------------- /src/helpers/Api.js: -------------------------------------------------------------------------------- 1 | function parseJSON(response) { 2 | return response.json() 3 | } 4 | 5 | function checkStatus(response) { 6 | if (response.status >= 200 && response.status < 300) { 7 | return response 8 | } 9 | 10 | const error = new Error(response.statusText) 11 | error.response = response 12 | throw error 13 | } 14 | 15 | export function request(url, options) { 16 | return fetch(url, options) 17 | .then(checkStatus) 18 | .then(parseJSON) 19 | } 20 | 21 | export function apiFetch(path, options) { 22 | const host = `${process.env.APP_HOST}:${process.env.APP_PORT}` 23 | const contextHost = (process.env.SERVER && host) || '' 24 | const apiUrl = `${contextHost}/api${path}&api_key=${process.env.API_KEY}` 25 | 26 | return fetch(apiUrl, options) 27 | .then(checkStatus) 28 | .then(parseJSON) 29 | } 30 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const compression = require('compression') 4 | const httpProxy = require('http-proxy') 5 | 6 | const app = express() 7 | const proxy = httpProxy.createProxyServer() 8 | 9 | app.use(compression()) 10 | app.disable('x-powered-by') 11 | app.use(express.static(path.join(__dirname, '..', '..', 'static'))) 12 | 13 | app.use('/api', (req, res) => { 14 | proxy.web(req, res, { target: process.env.API_URL, changeOrigin: true }) 15 | }) 16 | 17 | proxy.on('error', (error, req, res) => { 18 | if (error.code !== 'ECONNRESET') { 19 | console.error('proxy error', error) 20 | } 21 | if (!res.headersSent) { 22 | res.writeHead(500, { 'content-type': 'application/json' }) 23 | } 24 | 25 | res.end(JSON.stringify({ error: 'proxy_error', reason: error.message })) 26 | }) 27 | 28 | module.exports = { 29 | app 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/Hero/github.svg: -------------------------------------------------------------------------------- 1 | GitHub icon 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/SelectField/SelectField.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import type { FieldProps } from 'redux-form' 5 | 6 | type Props = { 7 | label: string, 8 | type: string, 9 | options: Array<{ value: string, title: string }> 10 | } & FieldProps 11 | 12 | const SelectField = ({ 13 | input, 14 | label, 15 | type, 16 | meta: { touched, error, warning }, 17 | options 18 | }: Props) => ( 19 |
20 | 35 |
36 | ) 37 | 38 | export default SelectField 39 | -------------------------------------------------------------------------------- /src/containers/Home/Home.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | import Helmet from 'react-helmet' 5 | import Link from 'react-router-dom/Link' 6 | 7 | const Home = () => ( 8 |
9 | 13 |

What is it really?

14 |

15 | A universally rendered PWA with code-splitting that uses: 16 |

17 |
    18 |
  • React
  • 19 |
  • React Router 4
  • 20 |
  • Redux
  • 21 |
  • Webpack
  • 22 |
  • Express
  • 23 |
  • Eslint
  • 24 |
  • Redux Form
  • 25 |
  • Style and Sass Loader
  • 26 |
  • RXJS
  • 27 |
  • Service Worker
  • 28 |
29 | 30 |

31 | Take a look at our Examples Page to see parts 32 | of this in action. 33 |

34 |
35 | ) 36 | 37 | export default Home 38 | -------------------------------------------------------------------------------- /src/containers/Hero/Hero.scss: -------------------------------------------------------------------------------- 1 | .Hero { 2 | text-align: center; 3 | } 4 | 5 | .Logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 200px; 8 | } 9 | 10 | .Header { 11 | background-color: #222; 12 | padding: 20px; 13 | color: white; 14 | } 15 | 16 | .Heading { 17 | font-size: 3em; 18 | margin-top: 10px; 19 | margin-bottom: 0; 20 | } 21 | 22 | .Intro { 23 | font-size: large; 24 | } 25 | 26 | .props { 27 | color: #999; 28 | font-size: 12px; 29 | } 30 | 31 | .link { 32 | color: #ddd; 33 | text-decoration: none; 34 | border-bottom: 1px solid gray; 35 | 36 | &:hover { 37 | color: #61dafb; 38 | } 39 | } 40 | 41 | .githubLink { 42 | composes: link; 43 | display: inline-block; 44 | font-size: 20px; 45 | color: #ddd; 46 | } 47 | 48 | .github { 49 | height: 20px; 50 | vertical-align: top; 51 | } 52 | 53 | @keyframes App-logo-spin { 54 | from { 55 | transform: rotate(0deg); 56 | } 57 | 58 | to { 59 | transform: rotate(360deg); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createStore, applyMiddleware, compose } from 'redux' 4 | import { createEpicMiddleware } from 'redux-observable' 5 | import app, { rootEpic } from './reducers' 6 | 7 | export default function configureStore( 8 | wrapEpic: Function => Function = f => f, 9 | data: {} 10 | ) { 11 | const wrappedEpic = wrapEpic(rootEpic) 12 | const epicMiddleware = createEpicMiddleware(wrappedEpic) 13 | const middleware = applyMiddleware(epicMiddleware) 14 | 15 | let store 16 | if (!process.env.SERVER) { 17 | const composeEnhancers = 18 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 19 | store = createStore(app, data, composeEnhancers(middleware)) 20 | } else { 21 | store = createStore(app, middleware) 22 | } 23 | 24 | if (module.hot) { 25 | module.hot.accept('./reducers', () => { 26 | store.replaceReducer(require('./reducers').default) 27 | }) 28 | } 29 | 30 | return { 31 | wrappedEpic, 32 | store 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/GithubButton/GithubButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react' 4 | 5 | type Props = { 6 | title: string, 7 | user: string, 8 | repo: string, 9 | type: 'star' | 'watch' | 'fork' | 'follow', 10 | width: number, 11 | height: number, 12 | count?: boolean, 13 | large?: boolean 14 | } 15 | 16 | const GithubButton = (props: Props) => { 17 | const { title, user, repo, type, width, height, count, large } = props 18 | let options = `?user=${user}&repo=${repo}&type=${type}` 19 | if (count) options += '&count=true' 20 | if (large) options += '&size=large' 21 | 22 | return ( 23 |