├── __mocks__ ├── styleMock.js └── fileMock.js ├── .eslintrc ├── src ├── server │ ├── routes │ │ ├── index.js │ │ └── api.js │ └── index.js └── app │ ├── components │ ├── Button │ │ ├── package.json │ │ ├── Button.story.js │ │ ├── Button.js │ │ ├── Button.css │ │ └── __tests__ │ │ │ └── Button.spec.js │ ├── Error │ │ ├── package.json │ │ ├── Error.css │ │ └── Error.js │ ├── Hero │ │ ├── package.json │ │ ├── slc.jpg │ │ ├── Hero.js │ │ ├── style.css │ │ └── Hero.story.js │ ├── Loader │ │ ├── package.json │ │ └── Loader.js │ ├── BigOList │ │ ├── package.json │ │ ├── BigOList.js │ │ ├── BigOList.css │ │ └── BigOList.story.js │ ├── SiteNav │ │ ├── package.json │ │ ├── SiteNav.css │ │ └── SiteNav.js │ ├── SiteHeader │ │ ├── package.json │ │ ├── SiteHeader.css │ │ ├── SiteHeader.js │ │ └── logo-placeholder.svg │ └── Welcome │ │ ├── assets │ │ └── sand.jpg │ │ └── Welcome.story.js │ ├── views │ ├── AppView │ │ ├── package.json │ │ ├── style.css │ │ └── AppView.js │ └── LandingView │ │ ├── package.json │ │ ├── style.css │ │ └── LandingView.js │ ├── hocs │ ├── append-lifecycle │ │ ├── package.json │ │ └── index.js │ └── ss-resolve │ │ ├── index.js │ │ ├── resolver.js │ │ ├── wrap.js │ │ └── readme.md │ ├── actions │ ├── page-meta.js │ ├── navigate.js │ ├── __tests__ │ │ ├── site-nav.spec.js │ │ ├── page-meta.spec.js │ │ └── system.spec.js │ ├── site-nav.js │ └── system.js │ ├── services │ └── site-nav.js │ ├── env.js │ ├── utils │ └── html-to-jsx.js │ ├── __test__ │ ├── html.spec.js │ ├── store.spec.js │ └── integration.test.js │ ├── reducers │ ├── site-nav.js │ ├── site-nav.spec.js │ ├── page-meta.js │ ├── __tests__ │ │ ├── page-meta.spec.js │ │ └── system.spec.js │ └── system.js │ ├── routes.js │ ├── _client.js │ ├── HTML.js │ ├── variables.css │ ├── containers │ ├── app.js │ ├── not-found.js │ └── home.js │ ├── store.js │ └── _server.js ├── .travis.yml ├── .gitignore ├── .storybook ├── config.js ├── webpack.config.js └── whitelister.js ├── .babelrc ├── .editorconfig ├── jest.config.js ├── .env ├── webpack.server.babel.js ├── LICENSE ├── webpack.client.babel.js ├── webpack.base.babel.js ├── README.md └── package.json /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | export {router as api} from './api.js'; 2 | -------------------------------------------------------------------------------- /src/app/components/Button/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "Button.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/Error/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "Error.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/Hero/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "Hero.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/Loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "Loader.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/views/AppView/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "AppView.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/BigOList/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "BigOList.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/SiteNav/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "SiteNav.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/hocs/append-lifecycle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/views/LandingView/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "LandingView.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/SiteHeader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "SiteHeader.js" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | after_success: 'npm run coveralls' -------------------------------------------------------------------------------- /src/app/views/AppView/style.css: -------------------------------------------------------------------------------- 1 | .app { 2 | 3 | } 4 | 5 | .header, 6 | .wrapper { 7 | padding: 1em; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/components/Hero/slc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxsudo/react-starter/HEAD/src/app/components/Hero/slc.jpg -------------------------------------------------------------------------------- /src/app/views/LandingView/style.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | margin-bottom: 1rem; 3 | } 4 | 5 | .link { 6 | float: right; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/components/Welcome/assets/sand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxsudo/react-starter/HEAD/src/app/components/Welcome/assets/sand.jpg -------------------------------------------------------------------------------- /src/app/hocs/ss-resolve/index.js: -------------------------------------------------------------------------------- 1 | import resolve from './resolver.js'; 2 | import wrap from './wrap.js'; 3 | 4 | 5 | export { resolve, wrap }; 6 | export default { resolve, wrap }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | .vscode/ 5 | .happypack 6 | 7 | # built files 8 | /dist 9 | static/assets 10 | static/app.* 11 | server/modules/react-server-app 12 | /coverage -------------------------------------------------------------------------------- /src/app/actions/page-meta.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_PAGE_META} from '../reducers/page-meta'; 2 | 3 | export const setPageMeta = ({title = "", meta = []} = {}) => ({ 4 | type: RECEIVE_PAGE_META, 5 | payload: {title, meta} 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/services/site-nav.js: -------------------------------------------------------------------------------- 1 | import {API_HOST} from '../env.js'; 2 | 3 | 4 | export default () => ( 5 | fetch(`${API_HOST}/api/nav`) 6 | .then(response => 7 | response.ok ? response.json() : Promise.reject(response) ) 8 | ); 9 | -------------------------------------------------------------------------------- /src/app/env.js: -------------------------------------------------------------------------------- 1 | import isBrowser from 'is-in-browser'; 2 | 3 | //Grab variables from process.env or window 4 | export const { 5 | APP_WEB_BASE_PATH, 6 | API_HOST, 7 | ALLOW_REDUX_DEV_TOOLS 8 | } = isBrowser ? window.__APP_ENV_VARS__ || {} : process.env; 9 | -------------------------------------------------------------------------------- /src/app/components/Error/Error.css: -------------------------------------------------------------------------------- 1 | .container { 2 | } 3 | 4 | .header { 5 | padding: 2vh 1vw; 6 | } 7 | 8 | .title, 9 | .subtitle { 10 | font-weight: normal; 11 | margin: 0; 12 | } 13 | 14 | .subtitle { 15 | font: normal .95em/1 "serif"; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/actions/navigate.js: -------------------------------------------------------------------------------- 1 | import {browserHistory} from 'react-router'; 2 | 3 | export const navigate = (url) => ( 4 | browserHistory.push(url) 5 | ); 6 | 7 | export const overrideAnchorClick = (e) => { 8 | navigate(e.target.href); 9 | e.preventDefault(); 10 | } 11 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@kadira/storybook'; 2 | 3 | 4 | function loadStories() { 5 | const req = require.context('../src/app', true, /.story.js$/); 6 | req.keys().forEach((filename) => req(filename)) 7 | } 8 | 9 | 10 | configure(loadStories, module); 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "latest", 4 | "stage-2", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "react-require" 9 | ], 10 | "env": { 11 | "test": { 12 | "presets": ["es2015", "react", "stage-2"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/utils/html-to-jsx.js: -------------------------------------------------------------------------------- 1 | // quickly turn HTML strings into a wrapped JSX component. 2 | // useful for when getting html strings from endpoint 3 | 4 | export const htmlToJsx = (__html) => ( 5 |
// eslint-disable-line 6 | ); 7 | 8 | export default htmlToJsx; 9 | -------------------------------------------------------------------------------- /src/app/__test__/html.spec.js: -------------------------------------------------------------------------------- 1 | import HTML from '../HTML'; 2 | 3 | test('html template', () => { 4 | const html = HTML({}); 5 | 6 | expect(typeof html).toBe('string'); 7 | expect(html).toContain('') 8 | expect(html).toContain('') 9 | expect(html).toContain('window.__INITIAL_STATE') 10 | }); -------------------------------------------------------------------------------- /src/app/actions/__tests__/site-nav.spec.js: -------------------------------------------------------------------------------- 1 | import { setNav } from '../site-nav.js'; 2 | import { RECEIVE_SITE_NAVIGATION } from '../../reducers/site-nav.js'; 3 | 4 | test('Actions setNav test', () => { 5 | expect( setNav(['foo', 'bar', 'ray']) ) 6 | .toEqual( {type: RECEIVE_SITE_NAVIGATION, nav: ['foo', 'bar', 'ray']} ); 7 | }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = false 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /src/app/reducers/site-nav.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_SITE_NAVIGATION = "RECEIVE_SITE_NAVIGATION"; 2 | 3 | export default function(state = [], {type, nav = [] } ) { 4 | switch(type) { 5 | case RECEIVE_SITE_NAVIGATION: 6 | return nav; 7 | 8 | default: 9 | return state; 10 | } 11 | } 12 | 13 | export const selectSiteNav = (state) => state; 14 | -------------------------------------------------------------------------------- /src/app/reducers/site-nav.spec.js: -------------------------------------------------------------------------------- 1 | import siteNanReducer, { RECEIVE_SITE_NAVIGATION } from './site-nav.js'; 2 | 3 | test('Reducers setNav test', () => { 4 | expect(siteNanReducer(undefined, {type: undefined})).toEqual([]); 5 | 6 | expect( 7 | siteNanReducer([], {type: RECEIVE_SITE_NAVIGATION, nav: ['foo', 'bar', 'baz']}) 8 | ).toEqual(['foo', 'bar', 'baz']); 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/actions/site-nav.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_SITE_NAVIGATION } from '../reducers/site-nav.js'; 2 | import getNav from '../services/site-nav.js'; 3 | 4 | 5 | 6 | export const setNav = ( nav=[] ) => { 7 | return { type: RECEIVE_SITE_NAVIGATION, nav }; 8 | } 9 | 10 | 11 | export const init = () => (dispatch) => ( 12 | getNav() 13 | .then( n => dispatch(setNav(n)) ) 14 | ); 15 | -------------------------------------------------------------------------------- /src/app/components/SiteNav/SiteNav.css: -------------------------------------------------------------------------------- 1 | @import 'variables.css'; 2 | 3 | .nav { 4 | display: flex; 5 | justify-content: space-between; 6 | } 7 | 8 | .link { 9 | color: var(--white); 10 | display: block; 11 | text-decoration: none; 12 | 13 | &:hover { 14 | text-decoration: underline; 15 | } 16 | 17 | &:not(:last-child) { 18 | margin-right: 1em; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/BigOList/BigOList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './BigOList.css'; 3 | import classes from 'join-classnames'; 4 | import {Children as childUtil} from 'react'; 5 | 6 | export const BigOList = ({children, className}) => ( 7 |
    8 | {childUtil.map(children, c => (
  1. {c}
  2. ) )} 9 |
10 | ) 11 | 12 | export default BigOList; 13 | -------------------------------------------------------------------------------- /src/app/components/SiteNav/SiteNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './SiteNav.css'; 3 | import { Link } from 'react-router'; 4 | import classes from 'join-classnames'; 5 | 6 | export default ({links=[], className=""}) => ( 7 | 12 | ); 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverage": false, 3 | "collectCoverageFrom": [ 4 | "src/**/*.{js,jsx}" 5 | ], 6 | "moduleFileExtensions": [ 7 | "js", 8 | "jsx" 9 | ], 10 | "moduleDirectories": ["node_modules", "bower_components", "shared"], 11 | "moduleNameMapper": { 12 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 13 | "\\.(css)$": "identity-obj-proxy" 14 | } 15 | }; -------------------------------------------------------------------------------- /src/app/reducers/page-meta.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_PAGE_META = "RECEIVE_PAGE_META"; 2 | 3 | export default (state = {}, {type, payload}) => { 4 | switch(type) { 5 | case RECEIVE_PAGE_META: { 6 | const {title, meta} = payload; 7 | return {title, meta} 8 | } 9 | 10 | default: 11 | return state; 12 | } 13 | } 14 | 15 | export const selectPageMeta = (state) => state; 16 | export const selectPageTitle = (state) => state.title; 17 | export const selectMetaTags = (state) => state.meta; 18 | -------------------------------------------------------------------------------- /src/app/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import App from './containers/app.js'; 4 | import Home from './containers/home.js'; 5 | import NotFound from './containers/not-found.js'; 6 | import {APP_WEB_BASE_PATH} from './env.js'; 7 | 8 | const routes = ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /src/app/views/AppView/AppView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'normalize.css'; 3 | import SiteHeader from '../../components/SiteHeader'; 4 | import styles from './style.css'; 5 | 6 | 7 | export default ({homelink, children, nav}) => ( 8 | 9 |
10 | 11 | 12 | 13 |
14 |
{children}
15 |
16 |
17 | ); 18 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # port the production app listens on 2 | PORT = 8888 3 | 4 | # change this if app is located somewhere other than root 5 | # exclude trailing slash 6 | APP_WEB_BASE_PATH = 7 | 8 | # change when endpoints vary based on env 9 | # eg: local, test, stage, prod... 10 | API_HOST = http://localhost:8888 11 | 12 | 13 | # Allow redux dev tools to be enabled/disabled by env var 14 | # 1: enabled 15 | # anything else: disabled 16 | ALLOW_REDUX_DEV_TOOLS = 1 17 | 18 | 19 | # DEV ONLY 20 | 21 | # port for webpack dev server 22 | WDS_PORT = 3000 23 | -------------------------------------------------------------------------------- /src/app/components/SiteHeader/SiteHeader.css: -------------------------------------------------------------------------------- 1 | @import '../../variables.css'; 2 | 3 | .header { 4 | align-items: center; 5 | background: var(--blue, green); 6 | color: var(--white, black); 7 | display: flex; 8 | justify-content: space-between; 9 | padding-bottom: 1em; 10 | padding-top: 1em; 11 | } 12 | 13 | .brand { 14 | color: var(--white); 15 | text-decoration: none; 16 | } 17 | 18 | .logo { 19 | fill: var(--white); 20 | height: 2em; 21 | margin-right: .5em; 22 | vertical-align: middle; 23 | width: 2em; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/Error/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Error.css'; 3 | 4 | const NotFound = ({title="Error...", subtitle, children}) => ( 5 |
6 |
7 |

Not Found

8 | {subtitle && ( 9 |

{subtitle}

10 | )} 11 |
12 |
{children}
13 |
14 | ); 15 | 16 | 17 | 18 | export default NotFound; 19 | -------------------------------------------------------------------------------- /src/server/routes/api.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | 4 | export const router = express.Router(); 5 | 6 | const {APP_WEB_BASE_PATH} = process.env; 7 | 8 | router.get('/nav', cors(), (req, res) => { 9 | res.send([ 10 | { 11 | "href": `${APP_WEB_BASE_PATH}/`, 12 | "text": "Home", 13 | "rel": "home" 14 | }, 15 | { 16 | "href": `${APP_WEB_BASE_PATH}/not-found`, 17 | "text": "404" 18 | } 19 | ]); 20 | }); 21 | 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/app/hocs/ss-resolve/resolver.js: -------------------------------------------------------------------------------- 1 | export default function(props, store) { 2 | 3 | const promises = (props.components||[]) 4 | // unwrap component if wrapped by react-redux bindings... 5 | .map(component => component.WrappedComponent || component) 6 | 7 | // grab only components with a static `load` method 8 | .filter(component => component.onServer) 9 | 10 | // execute onServer functions -- they should return a Promise 11 | .map(component => component.onServer(props, store)); 12 | 13 | return Promise.all(promises); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/hocs/ss-resolve/wrap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export const addServerSideResolve = (beforeServerResponse) => (Component) => { 5 | 6 | return React.createClass({ // eslint-disable-line 7 | statics: { 8 | onServer: function(props, store) { 9 | return beforeServerResponse ? beforeServerResponse(props, store) : null; 10 | } 11 | }, 12 | 13 | render: function() { 14 | return 15 | } 16 | }); 17 | }; 18 | 19 | export default addServerSideResolve; 20 | -------------------------------------------------------------------------------- /src/app/_client.js: -------------------------------------------------------------------------------- 1 | import { Router, browserHistory } from 'react-router'; 2 | import { syncHistoryWithStore } from 'react-router-redux'; 3 | import React from 'react'; 4 | import {render} from 'react-dom'; 5 | import {Provider} from 'react-redux'; 6 | 7 | import routes from './routes.js'; 8 | import getStore from './store.js'; 9 | 10 | const store = getStore(); 11 | const history = syncHistoryWithStore(browserHistory, store) 12 | 13 | render( 14 | 15 | 16 | , 17 | 18 | document.getElementById('app') 19 | ); 20 | -------------------------------------------------------------------------------- /src/app/views/LandingView/LandingView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Hero from '../../components/Hero'; 3 | import Button from '../../components/Button'; 4 | import styles from './style.css'; 5 | 6 | const LandingView = ({hero, title, subtitle, cta}) => ( 7 |
8 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | 23 | 24 | 25 | export default LandingView; 26 | -------------------------------------------------------------------------------- /src/app/__test__/store.spec.js: -------------------------------------------------------------------------------- 1 | import Store from '../store'; 2 | 3 | test("store setup", () => { 4 | const store = Store(); 5 | expect(typeof store).toBe('object') 6 | expect(typeof store.getState()).toBe('object') 7 | 8 | expect(store).toHaveProperty('dispatch') 9 | expect(store).toHaveProperty('getState') 10 | expect(store).toHaveProperty('replaceReducer') 11 | expect(store).toHaveProperty('subscribe') 12 | 13 | expect(store.getState()).toHaveProperty('nav') 14 | expect(store.getState()).toHaveProperty('pageMeta') 15 | expect(store.getState()).toHaveProperty('routing') 16 | expect(store.getState()).toHaveProperty('system') 17 | 18 | }); -------------------------------------------------------------------------------- /webpack.server.babel.js: -------------------------------------------------------------------------------- 1 | import base from './webpack.base.babel.js'; 2 | import path from 'path'; 3 | import nodeExternals from 'webpack-node-externals'; 4 | 5 | const {APP_WEB_BASE_PATH} = process.env; 6 | 7 | export default { 8 | 9 | ...base, 10 | 11 | entry: path.resolve('./src/server/index.js'), 12 | 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: "server.js", 16 | publicPath: `${APP_WEB_BASE_PATH}/` 17 | }, 18 | 19 | externals: [ nodeExternals({ 20 | whitelist: ['normalize.css'] 21 | })], 22 | 23 | target: 'node', 24 | 25 | node: { 26 | __filename: false, 27 | __dirname: false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/components/SiteHeader/SiteHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './SiteHeader.css'; 3 | import SiteNav from '../SiteNav/SiteNav'; 4 | import classes from 'join-classnames'; 5 | import logo from './logo-placeholder.svg'; 6 | import {IndexLink} from 'react-router'; 7 | 8 | export default ({homelink="/", links = [], className=""}) => ( 9 |
10 | 11 | My Brand 12 | React Starter 13 | 14 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /src/app/components/SiteHeader/logo-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/actions/__tests__/page-meta.spec.js: -------------------------------------------------------------------------------- 1 | import { setPageMeta } from '../page-meta'; 2 | import { RECEIVE_PAGE_META } from '../../reducers/page-meta'; 3 | 4 | test.only('Actions: page meta', () => { 5 | const meta = { 6 | title: 'some page title', 7 | meta: 'some page meta' 8 | }; 9 | 10 | expect( setPageMeta(meta) ) 11 | .toEqual({ 12 | type: RECEIVE_PAGE_META, 13 | payload: { 14 | title: 'some page title', 15 | meta: 'some page meta' 16 | } 17 | }); 18 | 19 | expect(setPageMeta()) 20 | .toEqual({ 21 | type: RECEIVE_PAGE_META, 22 | payload: { 23 | title: "", 24 | meta: [] 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/reducers/__tests__/page-meta.spec.js: -------------------------------------------------------------------------------- 1 | import reducer, { 2 | selectPageMeta, 3 | selectPageTitle, 4 | selectMetaTags, 5 | RECEIVE_PAGE_META 6 | } from '../page-meta'; 7 | 8 | test('Action: page-meta', ()=>{ 9 | 10 | const state = { 11 | title: 'a title', 12 | meta: [{a:1}] 13 | } 14 | 15 | const action = { 16 | type: RECEIVE_PAGE_META 17 | } 18 | 19 | expect(selectPageMeta(state)) 20 | .toEqual(state); 21 | 22 | expect(selectPageTitle(state)) 23 | .toBe(state.title); 24 | 25 | expect(selectMetaTags(state)) 26 | .toBe(state.meta); 27 | 28 | expect(reducer(undefined,{})) 29 | .toEqual({}) 30 | 31 | expect(reducer(undefined, {...action, payload: {...state}} )) 32 | .toEqual(state) 33 | 34 | }); -------------------------------------------------------------------------------- /src/app/components/BigOList/BigOList.css: -------------------------------------------------------------------------------- 1 | @import '../../variables.css'; 2 | 3 | .list { 4 | counter-reset: olist-counter; 5 | margin: 0; 6 | 7 | } 8 | 9 | .item { 10 | align-items: center; 11 | display: flex; 12 | list-style: none; 13 | padding: 1em 0; 14 | 15 | &:before, 16 | &:after { 17 | font-size: 3em; 18 | padding: 0 1em; 19 | 20 | } 21 | 22 | &:nth-child(even) { 23 | justify-content: flex-end; 24 | } 25 | 26 | 27 | &:nth-child(odd):before { 28 | content: counter(olist-counter, decimal); 29 | counter-increment: olist-counter; 30 | } 31 | 32 | &:nth-child(even):after { 33 | content: counter(olist-counter, decimal); 34 | counter-increment: olist-counter; 35 | } 36 | 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/components/Button/Button.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import Button from './Button'; 4 | 5 | storiesOf('Button', module) 6 | 7 | .add('default', () => ()) 8 | 9 | .add('primary', () => ( 10 | 11 | )) 12 | 13 | .add('secondary', () => ( 14 | 15 | )) 16 | 17 | .add('warn', () => ( 18 | 19 | )) 20 | 21 | .add('error', () => ( 22 | 23 | )) 24 | 25 | .add('disabled', () => ( 26 | 27 | )); 28 | -------------------------------------------------------------------------------- /src/app/components/Hero/Hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './style.css'; 3 | import classes from 'join-classnames'; 4 | 5 | export const Hero = ({title, subtitle, bgSrc, overlay=false, href, onClick, className}) => ( 6 | 12 | 13 | {overlay && } 14 | 15 |

16 | {title} 17 | {subtitle && {subtitle}} 18 |

19 | 20 | 21 |
22 | ); 23 | 24 | 25 | export default Hero; 26 | -------------------------------------------------------------------------------- /src/app/HTML.js: -------------------------------------------------------------------------------- 1 | export default ({title="", meta="", links="", content="", initialState={}, env={}, base_path="" }) => ` 2 | 3 | 4 | 5 | ${title} 6 | 7 | ${meta} 8 | ${links} 9 | 10 | 11 | 12 | 13 | 14 |
${content}
15 | 16 | 20 | 21 | 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /src/app/actions/system.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_HTTP_RESPONSE_CODE, 3 | RECEIVE_HTTP_RESPONSE_CODE_RESET, 4 | RECEIVE_APPLICATION_ERROR_RESET, 5 | RECEIVE_APPLICATION_ERROR, 6 | RECEIVE_APPLICATION_ERROR_REMOVAL 7 | } from '../reducers/system.js'; 8 | 9 | export const setHttpResponseCode = (code) => ({ 10 | type: RECEIVE_HTTP_RESPONSE_CODE, 11 | payload: code 12 | }); 13 | 14 | export const resetHttpResponseCode = () => ({ 15 | type: RECEIVE_HTTP_RESPONSE_CODE_RESET 16 | }); 17 | 18 | export const resetApplicationErrors = () => ({ 19 | type: RECEIVE_APPLICATION_ERROR_RESET 20 | }); 21 | 22 | export const addApplicationError = ({id, title, details, date}) => ({ 23 | type: RECEIVE_APPLICATION_ERROR, 24 | payload: {id, title, details, date} 25 | }); 26 | 27 | export const removeApplicationError = ({id}) => ({ 28 | type: RECEIVE_APPLICATION_ERROR_REMOVAL, 29 | payload: {id} 30 | }); 31 | -------------------------------------------------------------------------------- /src/app/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Button.css'; 3 | import classes from 'join-classnames'; 4 | 5 | 6 | const determineEmphasis = emphasis => { 7 | 8 | switch(emphasis) { 9 | 10 | case "error": 11 | case "warn": 12 | case "success": 13 | return emphasis; 14 | 15 | case false: 16 | case "secondary": 17 | return "secondary"; 18 | 19 | default: 20 | return "primary"; 21 | 22 | } 23 | } 24 | 25 | 26 | export default ({onClick, type="button", emphasis="primary", disabled=false, children, value="Submit", className}) => ( 27 | 35 | 36 | ); 37 | -------------------------------------------------------------------------------- /src/app/components/BigOList/BigOList.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | import BigOList from './BigOList'; 4 | 5 | storiesOf('BigOList', module) 6 | 7 | .add('default', () => ( 8 | 9 |
Nostrud ut consectetur eiusmod est excepteur irure aliqua anim sunt ullamco culpa commodo aute velit nulla Lorem duis.
10 |
Incididunt adipisicing voluptate minim consectetur sit fugiat elit irure elit laborum commodo velit incididunt aliquip et excepteur sunt.
11 |
Amet elit officia proident ad velit cillum id sint.
12 |
Occaecat dolor minim labore do qui duis cillum nisi veniam aute mollit id tempor aliquip.
13 |
Officia esse tempor veniam cupidatat sunt amet fugiat fugiat nostrud amet consectetur aute sint nisi minim.
14 |
Incididunt sunt qui velit sunt duis pariatur non sunt.
15 |
16 | )); 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jared Anderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/app/__test__/integration.test.js: -------------------------------------------------------------------------------- 1 | import Store from '../store' 2 | import { setNav } from '../actions/site-nav' 3 | import { setPageMeta } from '../actions/page-meta' 4 | import { setHttpResponseCode } from '../actions/system' 5 | 6 | const store = Store() 7 | 8 | test('store exists', () => { 9 | expect(typeof store).toBe('object') 10 | }) 11 | 12 | test('nav action → store', done => { 13 | const nav = ['home', 'about'] 14 | store.subscribe(() => { 15 | const state = store.getState() 16 | expect(state.nav).toBe(nav) 17 | done() 18 | }) 19 | 20 | store.dispatch(setNav(nav)) 21 | }) 22 | 23 | test('page meta action → store', done => { 24 | store.subscribe(() => { 25 | const state = store.getState() 26 | expect(state.pageMeta.title).toBe('test title') 27 | done() 28 | }) 29 | 30 | store.dispatch(setPageMeta({ 31 | title: 'test title', 32 | meta: [{'og-description': 'page description'}] 33 | })) 34 | }) 35 | 36 | test('system action → store', done => { 37 | store.subscribe(() => { 38 | const state = store.getState() 39 | expect(state.system.httpResponse).toBe(451) 40 | done() 41 | }) 42 | 43 | store.dispatch(setHttpResponseCode(451)) 44 | }) -------------------------------------------------------------------------------- /src/app/components/Button/Button.css: -------------------------------------------------------------------------------- 1 | @import '../../variables.css'; 2 | 3 | .button { 4 | background-color: var(--white, white); 5 | border: 0; 6 | border-radius: .25em; 7 | cursor: pointer; 8 | font-size: 1em; 9 | line-height: 1.25em; 10 | outline: none; 11 | padding: .25em .5em; 12 | 13 | &[disabled] { 14 | background-color: var(--lightgray, lightgray); 15 | color: var(--white, white); 16 | pointer-events: none; 17 | } 18 | 19 | &:hover { 20 | filter: brightness(110%); 21 | transition: filter .1s linear; 22 | } 23 | 24 | &:active { 25 | transform: scale(.95); 26 | transition: transform .1s linear; 27 | } 28 | } 29 | 30 | .primary { 31 | background-color: var(--blue, blue); 32 | color: var(--white, white); 33 | } 34 | 35 | .secondary { 36 | background-color: var(--black, black); 37 | color: var(--white, white); 38 | } 39 | 40 | .success { 41 | background-color: var(--green, green); 42 | color: var(--white, white); 43 | } 44 | 45 | .error { 46 | background-color: var(--red, red); 47 | color: var(--white, white); 48 | } 49 | 50 | .warn { 51 | background-color: var(--orange, orange); 52 | color: var(--white, white); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/actions/__tests__/system.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_HTTP_RESPONSE_CODE, 3 | RECEIVE_HTTP_RESPONSE_CODE_RESET, 4 | RECEIVE_APPLICATION_ERROR_RESET, 5 | RECEIVE_APPLICATION_ERROR, 6 | RECEIVE_APPLICATION_ERROR_REMOVAL 7 | } from '../../reducers/system.js'; 8 | 9 | import { 10 | setHttpResponseCode, 11 | resetHttpResponseCode, 12 | resetApplicationErrors, 13 | addApplicationError, 14 | removeApplicationError 15 | } from '../system'; 16 | 17 | test('Actions: system', () => { 18 | 19 | expect(setHttpResponseCode(200)).toEqual({ 20 | type: RECEIVE_HTTP_RESPONSE_CODE, 21 | payload: 200 22 | }) 23 | 24 | expect(resetHttpResponseCode()).toEqual({ 25 | type: RECEIVE_HTTP_RESPONSE_CODE_RESET 26 | }) 27 | 28 | expect(resetApplicationErrors()).toEqual({ 29 | type: RECEIVE_APPLICATION_ERROR_RESET 30 | }) 31 | 32 | const error = { 33 | id: 1, 34 | title: 'whoops!', 35 | details: 'it bork', 36 | date: new Date() 37 | }; 38 | 39 | expect(addApplicationError(error)).toEqual({ 40 | type: RECEIVE_APPLICATION_ERROR, 41 | payload: error 42 | }) 43 | 44 | expect(removeApplicationError({id: error.id})).toEqual({ 45 | type: RECEIVE_APPLICATION_ERROR_REMOVAL, 46 | payload: {id: error.id} 47 | }) 48 | 49 | }); -------------------------------------------------------------------------------- /webpack.client.babel.js: -------------------------------------------------------------------------------- 1 | import base from './webpack.base.babel.js'; 2 | import path from 'path'; 3 | import webpack from 'webpack'; 4 | 5 | const {WDS_PORT, PORT, APP_WEB_BASE_PATH} = process.env; 6 | 7 | export default { 8 | ...base, 9 | 10 | entry: "./src/app/_client.js", 11 | output: { 12 | path: path.join(__dirname, 'dist', 'static'), 13 | filename: "app.js", 14 | publicPath: `${APP_WEB_BASE_PATH}/` 15 | }, 16 | 17 | plugins: base.plugins 18 | .concat(process.env.NODE_ENV==="production" 19 | ? [ 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.optimize.UglifyJsPlugin({ 22 | compressor: { 23 | warnings: false 24 | }, 25 | sourcemaps: true 26 | }) 27 | ] 28 | : [] 29 | ), 30 | 31 | devServer: { 32 | publicPath: '/static/', 33 | contentBase: `http://localhost:${PORT}/static`, 34 | historyApiFallback: true, 35 | progress: false, 36 | stats: 'errors-only', 37 | compress: true, 38 | port: WDS_PORT, 39 | proxy: { 40 | "**": `http://localhost:${PORT}` 41 | } 42 | } 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #0078e7; 3 | --green: #2AA876; 4 | --yellow: #FFD265; 5 | --orange: #F19C65; 6 | --red: #CE4D45; 7 | --black: #1e1e20; 8 | --white: #FFFFFA; 9 | --gray: #5B605F; 10 | --darkgray: #2A2C2B; 11 | --lightgray: #A1A194; 12 | 13 | --serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif; 14 | --sans: 'Lucida Sans Unicode', 'Lucida Grande', sans-serif; 15 | 16 | 17 | @custom-media --xs-min (width >= 320px); 18 | @custom-media --xs-max (width < 320px); 19 | @custom-media --xs-only (width >= 320px) and (width < 500px); 20 | 21 | @custom-media --s-min (width >= 500px); 22 | @custom-media --s-max (width < 500px); 23 | @custom-media --s-only (width >= 500) and (width < 640); 24 | 25 | @custom-media --m-min (width >= 640px); 26 | @custom-media --m-max (width < 640px); 27 | @custom-media --m-only (width >= 640) and (width < 900); 28 | 29 | @custom-media --l-min (width >= 900px); 30 | @custom-media --l-max (width < 900px); 31 | @custom-media --l-only (width >= 900px) and (width < 1200px); 32 | 33 | @custom-media --xl-min (width >= 1200px); 34 | @custom-media --xl-max (width < 1200px); 35 | @custom-media --xl-only (width >= 1200px) and (width < 1500px); 36 | 37 | @custom-media --xxl-min (width >= 1500px); 38 | @custom-media --xxl-max (width < 1500px); 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/components/Loader/Loader.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | export default class extends Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | 8 | let {loading, error, wait} = { 9 | loading: true, 10 | error: false, 11 | wait: false, 12 | ...props 13 | }; 14 | 15 | this.state = { wait, loading, error }; 16 | } 17 | 18 | componentWillMount() { 19 | const onLoad = this.props.onLoad(); 20 | 21 | if(onLoad.then && onLoad.catch) { 22 | onLoad 23 | .then(() => this.setState({loading: false, error: false})) 24 | .catch( ({message}) => this.setState({loading: false, error: true, message})); 25 | } 26 | } 27 | 28 | renderStatus() { 29 | const { error, loading } = this.state; 30 | 31 | return ( 32 |
33 |

34 | {error && "An Error Occurred"} 35 | {loading && "Loading..."} 36 |

37 |
38 | ); 39 | } 40 | 41 | 42 | render() { 43 | const { wait, error, loading } = this.state; 44 | const {children} = this.props; 45 | 46 | if(wait && (error || loading) ) { 47 | return this.renderStatus(); 48 | } 49 | 50 | return children; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/Welcome/Welcome.story.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | import Hero from '../Hero'; 4 | import BigOList from '../BigOList'; 5 | import imgsrc from './assets/sand.jpg'; 6 | 7 | storiesOf('StoryBook', module).add('welcome', () => ( 8 |
9 | 15 | 16 | 17 |
18 |

Create a Component

19 |

components are found in ./src/app/components

20 |
21 |
22 |

Write a Component Story

23 |

24 | Component stories are just a bunch of usage examples of a component. 25 | Learn how to write stories. 26 |

27 |
28 |
29 |

Add Story to the StoryBook Config

30 |

if you name your story file like, `component.story.js`, you can skip this step. othewise, add the the appropriate `require` statement in the config file found in `./.storybook/config.js`.

31 |
32 |
33 | 34 |
35 | )); 36 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import 'isomorphic-fetch'; 3 | import express from 'express'; 4 | import path from 'path'; 5 | import compression from 'compression'; 6 | import helmet from 'helmet'; 7 | import bodyParser from 'body-parser'; 8 | import {api} from './routes'; 9 | 10 | // the reactified route-handler from the `app` 11 | import reactHandler from '../app/_server.js'; 12 | 13 | // create express app... 14 | export const app = express(); 15 | 16 | const {APP_WEB_BASE_PATH} = process.env; 17 | 18 | 19 | // middleware 20 | app.use(compression()); 21 | app.use(helmet()); 22 | app.use(`${APP_WEB_BASE_PATH}/static`, express.static(path.join(__dirname, 'static'))); 23 | app.use(bodyParser.urlencoded({extended:false})); 24 | app.use(bodyParser.json()); 25 | 26 | app.use(`${APP_WEB_BASE_PATH}/api`, api); 27 | 28 | 29 | // handle routes via react... 30 | app.get("*", reactHandler); 31 | 32 | 33 | // prepare 404 34 | app.use("*", (req, res, next) => { // eslint-disable-line 35 | next({status: 404, message: "Not Found"}); 36 | }); 37 | 38 | 39 | // handle any errors 40 | app.use( (err, req, res, next) => { // eslint-disable-line 41 | res.status(err.status||500).send(err.message || "Application Error"); 42 | console.error(err.status===404?`404 ${req.url}`: err.stack); // eslint-disable-line 43 | }); 44 | 45 | const { PORT } = process.env; 46 | 47 | app.listen(PORT, () => console.log('Running on port ' + PORT)); // eslint-disable-line 48 | -------------------------------------------------------------------------------- /src/app/components/Hero/style.css: -------------------------------------------------------------------------------- 1 | @import '../../variables.css'; 2 | 3 | .hero { 4 | align-items: flex-end; 5 | background-color: var(--lightgray, lightgray); 6 | background-position: center center; 7 | background-size: cover; 8 | display: flex; 9 | justify-content: flex-end; 10 | flex-flow: column nowrap; 11 | min-height: 25vw; 12 | margin: 0; 13 | padding: 5em 1em 1em; 14 | position: relative; 15 | overflow: hidden; 16 | text-decoration: none; 17 | 18 | & > * { 19 | z-index: 1; 20 | } 21 | 22 | &.clickable { 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | 28 | 29 | .overlay { 30 | background-color: #0006; 31 | bottom: 0; 32 | left: 0; 33 | position: absolute; 34 | right: 0; 35 | top: 0; 36 | z-index: 0; 37 | 38 | &.light { 39 | background-color: #0003; 40 | } 41 | 42 | &.dark { 43 | background-color: #0009; 44 | } 45 | } 46 | 47 | .title { 48 | color: var(--white, white); 49 | font: 2em/1.25 normal var(--serif, serif); 50 | text-align: right; 51 | margin: 0; 52 | 53 | @media (--l-min) { 54 | font-size: 3em; 55 | } 56 | } 57 | 58 | .subtitle { 59 | display: block; 60 | font-family: var(--sans, sans); 61 | font-size: .5em; 62 | margin-top: .25em; 63 | 64 | @media (--l-min) { 65 | margin-top: 0; 66 | } 67 | 68 | } 69 | 70 | .action { 71 | margin: 1em 0; 72 | } 73 | -------------------------------------------------------------------------------- /src/app/components/Button/__tests__/Button.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '../Button'; 3 | import styles from '../Button.css'; 4 | import {spy} from 'sinon'; 5 | import { shallow } from 'enzyme'; 6 | 7 | 8 | test('