├── __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 => ({c} ) )}
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 |
8 | {links.map( ({href, text}, i) => (
9 | {text})
10 | )}
11 |
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 |
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 |
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', () => (Default ))
8 |
9 | .add('primary', () => (
10 | Primary
11 | ))
12 |
13 | .add('secondary', () => (
14 | Secondary
15 | ))
16 |
17 | .add('warn', () => (
18 | Warn
19 | ))
20 |
21 | .add('error', () => (
22 | Error
23 | ))
24 |
25 | .add('disabled', () => (
26 | Disabled
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 |
33 | {children || value}
34 |
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(' ', () => {
9 | expect(shallow( ).text()==="Go!").toBeTruthy();
10 |
11 | expect(shallow( ).text()==="Submit").toBeTruthy();
12 |
13 | expect(shallow( ).prop('type')==="submit").toBeTruthy();
14 |
15 | expect(shallow( ).prop('type')==="button").toBeTruthy();
16 |
17 | expect(shallow( ).hasClass(styles.primary)).toBeTruthy();
18 |
19 | expect(shallow( ).hasClass(styles.primary)).toBeTruthy();
20 |
21 | expect(shallow( ).hasClass(styles.secondary)).toBeTruthy();
22 |
23 | expect(shallow( ).hasClass(styles.success)).toBeTruthy();
24 |
25 | expect(shallow( ).hasClass(styles.error)).toBeTruthy();
26 |
27 | expect(shallow( ).hasClass(styles.warn)).toBeTruthy();
28 |
29 | expect(shallow( ).hasClass("myClass")).toBeTruthy();
30 |
31 | expect(shallow( ).prop('disabled')===true).toBeTruthy();
32 |
33 |
34 | const clickHandler = spy();
35 | shallow( ).simulate('click');
36 | expect(clickHandler.callCount===1).toBeTruthy();
37 |
38 | expect(3).toBe(3)
39 | });
40 |
--------------------------------------------------------------------------------
/src/app/hocs/append-lifecycle/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | export const appendLifeCycle = ({
4 | componentWillMount,
5 | componentDidMount,
6 | componentWillReceiveProps,
7 | shouldComponentUpdate,
8 | componentWillUpdate,
9 | componentDidUpdate,
10 | componentWillUnmount
11 | }) => (LambdaComponent) => (
12 |
13 | class AppendedLifeCycle extends Component {
14 |
15 | componentWillMount() {
16 | return componentWillMount && componentWillMount.call(this);
17 | }
18 |
19 | componentDidMount() {
20 | return componentDidMount && componentDidMount.call(this);
21 | }
22 |
23 | componentWillReceiveProps(nextProps) {
24 | return componentWillReceiveProps && componentWillReceiveProps.call(this, nextProps);
25 | }
26 |
27 | shouldComponentUpdate(nextProps, nextState) {
28 | return shouldComponentUpdate
29 | ? shouldComponentUpdate.call(this, nextProps, nextState)
30 | : true;
31 | }
32 |
33 | componentWillUpdate(nextProps, nextState) {
34 | return componentWillUpdate && componentWillUpdate.call(this, nextProps, nextState);
35 | }
36 |
37 | componentDidUpdate(prevProps, prevState) {
38 | return componentDidUpdate && componentDidUpdate.call(this, prevProps, prevState);
39 | }
40 |
41 | componentWillUnmount() {
42 | return componentWillUnmount && componentWillUnmount.call(this);
43 | }
44 |
45 | render() {
46 | return
47 | }
48 |
49 | }
50 | )
51 |
52 | export default appendLifeCycle;
53 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const cssnext = require('postcss-cssnext');
2 | const cssimport = require('postcss-import');
3 | const path = require('path');
4 | const whiteLister = require('./whitelister.js');
5 |
6 |
7 |
8 | module.exports = {
9 | module: {
10 | loaders: [
11 | {
12 | test: /\.js$/,
13 | loader: 'babel-loader?cacheDirectory',
14 | exclude: whiteLister([
15 | 'react',
16 | 'compression',
17 | 'cors',
18 | 'dotenv',
19 | 'express',
20 | 'html-minifier',
21 | 'npm-run-all',
22 | 'string-hash'
23 | ])
24 | },
25 | {
26 | test: /\.css?$/,
27 | loaders: ['style', 'css-loader?modules&importLoaders=1&localIdentName=[local]-[hash:base64:5]', 'postcss-loader'],
28 | include: path.resolve(__dirname, '../')
29 | },
30 |
31 | {
32 | test : /\.json$/,
33 | loader : 'json'
34 | },
35 |
36 | {
37 | test: /\.(png|jpe?g|gif|svg|mp3|mpe?g)$/,
38 | loader: "file-loader?name=static/assets/[name]-[hash:2].[ext]"
39 | }
40 |
41 | ]
42 |
43 | },
44 |
45 |
46 | cssLoader: {
47 | modules: true
48 | },
49 |
50 | postcss : [
51 | cssimport({path: path.normalize(`${__dirname}/../src/app`)}),
52 | cssnext()
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/src/app/hocs/ss-resolve/readme.md:
--------------------------------------------------------------------------------
1 | # React / React-Router Server-Side Resolver
2 |
3 | Create a server-ready `React.Component` from a normal `React.Component` and a promise-based function. Server will
4 | wait for promise-based function to resolve before responding...
5 |
6 | Useful for delaying server responses until async `props` are resolved.
7 |
8 |
9 | ## Example
10 |
11 | Create a server-ready route-component derived from any regular component using `wrap`:
12 | ```
13 | // my-container.js
14 | import { wrap } from './ss-resolve';
15 | import RegularComponent from './my-components/RegularComponent';
16 | import action from './my-actions/action.js';
17 | import fetchData from './my-services/data.js';
18 |
19 | const beforeServerRender = (props, store) => (
20 | fetchData(props.params)
21 | .then(data => store.dispatch(action(data)))
22 | );
23 |
24 | export wrap(RegularComponent, beforeServerRender);
25 | ```
26 |
27 | In a server-side route, use `resolve` to trigger those promise-based callbacks:
28 | ```
29 | // server.js
30 | import { createStore } from 'redux'
31 | import reducers from '../path/to/reducers';
32 | import { match } from 'react-router';
33 | // ...other stuff found server-side
34 |
35 | app.get('*', (req, res, next) => {
36 |
37 | match({ routes, location: req.url }, (err, redirect, props) => {
38 |
39 | //... handle errors and redirects
40 |
41 | // otherwise:
42 | const store = createStore(reducers);
43 |
44 | resolve(props, store)
45 | .then( () => {
46 | res.send( store.getState() )
47 | })
48 |
49 | })
50 | });
51 | ```
52 |
--------------------------------------------------------------------------------
/src/app/containers/app.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import Helmet from 'react-helmet';
4 | import { init as initNav } from '../actions/site-nav.js';
5 | import AppView from '../views/AppView';
6 | import { selectSiteNav, selectPageMeta } from '../store.js';
7 |
8 |
9 | // argument 1 of react-redux `connect` maps store data to props
10 | const mapStateToProps = (state) => {
11 | const nav = selectSiteNav(state);
12 | const {title, meta} = selectPageMeta(state);
13 | return {
14 | nav,
15 | homelink: (nav.find(n=>n.rel==="home")||{}).href,
16 | title, meta
17 | }
18 | };
19 |
20 | // argument 2 of react-redux `connect` maps actions to dispatch to props
21 | const bindActionsToDispatch = ({initNav});
22 |
23 | // create the store connector HoC
24 | const storeConnector = connect(
25 | mapStateToProps,
26 | bindActionsToDispatch
27 | );
28 |
29 | // create the container
30 | class AppContainer extends Component {
31 |
32 | // if a promise is returned, server will wait
33 | // to send response...
34 | static onServer(props, store) {
35 | return store.dispatch( initNav() );
36 | }
37 |
38 | componentDidMount() {
39 | return this.props.initNav();
40 | }
41 |
42 | render() {
43 | const {children, title, meta, ...props} = this.props;
44 | return (
45 |
46 |
47 | {children}
48 |
49 | );
50 | }
51 |
52 | }
53 |
54 |
55 | // Export the connected, container component...
56 | export default storeConnector(AppContainer);
57 |
--------------------------------------------------------------------------------
/src/app/containers/not-found.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { setHttpResponseCode } from '../actions/system';
4 | import { setPageMeta } from '../actions/page-meta';
5 | import NotFound from '../components/Error';
6 |
7 | const pageMeta = {
8 | title: "Page Not Found :(",
9 | meta: [
10 | {"name": "description", "content": "This page was not found or an error occured"},
11 | {"property": "og:type", "content": "article"}
12 | ]
13 | };
14 |
15 | // takes values from the redux store and maps them to props
16 | const mapStateToProps = state => ({
17 | //propName: state.data.specificData
18 | });
19 |
20 | // binds the result of action creators to redux dispatch, wrapped in callable functions
21 | const bindActionsToDispatch = dispatch => ({
22 | setPageMeta: (meta) => { dispatch(setPageMeta(meta)) }
23 | });
24 |
25 | const mergeAllProps = (state, actions) => ({
26 | init: () => actions.setPageMeta(pageMeta),
27 | title: "Page Not Found",
28 | subtitle: "Sorry Not Sorry"
29 | })
30 |
31 | const storeConnector = connect(
32 | mapStateToProps,
33 | bindActionsToDispatch,
34 | mergeAllProps
35 | );
36 |
37 | class NotFoundContainer extends Component {
38 |
39 | static onServer(props, store) {
40 | return Promise.all([
41 | store.dispatch(setPageMeta(pageMeta)),
42 | store.dispatch(setHttpResponseCode(404))
43 | ]);
44 | }
45 |
46 | componentDidMount() {
47 | this.props.init();
48 | }
49 |
50 | render() {
51 | return
52 | }
53 |
54 | }
55 |
56 | export default storeConnector(NotFoundContainer);
57 |
--------------------------------------------------------------------------------
/src/app/containers/home.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import LandingView from '../views/LandingView';
3 | import { connect } from 'react-redux';
4 | import { setPageMeta } from '../actions/page-meta';
5 |
6 | const pageMeta = {
7 | title: "Homepage, yo!!",
8 | meta: [
9 | {"name": "description", "content": "A React Starter"},
10 | {"property": "og:type", "content": "article"}
11 | ]
12 | };
13 |
14 | // takes values from the redux store and maps them to props
15 | const mapStateToProps = state => ({
16 | //propName: state.data.specificData
17 | });
18 |
19 | // binds the result of action creators to redux dispatch, wrapped in callable functions
20 | const bindActionsToDispatch = dispatch => ({
21 | setPageMeta: (meta) => { dispatch(setPageMeta(meta)) }
22 | });
23 |
24 | // takes the result of mapStateToProps as store, and bindActionsToDispatch as actions
25 | // returns the final resulting props which will be passed to the component
26 | const mergeAllProps = (store, actions) => ({
27 | init: () => actions.setPageMeta(pageMeta),
28 | title: "React/Redux Starter",
29 | subtitle: "for isounimorphic applications",
30 | hero: "http://lorempixel.com/1200/500/",
31 | cta: "https://github.com/tuxsudo/react-starter"
32 | });
33 |
34 |
35 | const storeConnector = connect(
36 | mapStateToProps,
37 | bindActionsToDispatch,
38 | mergeAllProps
39 | );
40 |
41 |
42 |
43 | class HomeContainer extends Component {
44 |
45 | static onServer(props, store) {
46 | return store.dispatch(setPageMeta(pageMeta))
47 | }
48 |
49 | componentDidMount() {
50 | this.props.init();
51 | }
52 |
53 | render() {
54 | return
55 | }
56 |
57 | }
58 |
59 | export default storeConnector(HomeContainer);
60 |
--------------------------------------------------------------------------------
/webpack.base.babel.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 | import cssnext from 'postcss-cssnext';
3 | import cssimport from 'postcss-import';
4 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
5 | import webpack from 'webpack'
6 | import HappyPack from 'happypack';
7 |
8 |
9 | export default {
10 |
11 | module: {
12 |
13 | devtool: 'source-map',
14 |
15 | debug: true,
16 |
17 | loaders: [
18 | {
19 | test: /\.css$/,
20 | loader: ExtractTextPlugin.extract(
21 | "style-loader",
22 | process.env.NODE_ENV==="production"
23 | ? "css-loader?minimize&modules&importLoaders=1&localIdentName=[local]-[hash:base64:5]!postcss-loader"
24 | : "css-loader?modules&importLoaders=1&localIdentName=[local]-[hash:base64:5]!postcss-loader"
25 | )
26 | },
27 |
28 | {
29 | test : /\.js$/,
30 | loaders: [ 'happypack/loader' ] // replaced... loader : 'babel-loader'
31 | },
32 |
33 | {
34 | test : /\.json$/,
35 | loader : 'json-loader'
36 | },
37 |
38 | {
39 | test: /\.(png|jpe?g|gif|svg|mp3|mpe?g)$/,
40 | loader: "file-loader?name=static/assets/[name]-[hash:2].[ext]"
41 | }
42 |
43 | ]
44 |
45 | },
46 |
47 | plugins: [
48 | new ExtractTextPlugin("app.css"),
49 | new webpack.DefinePlugin({
50 | 'process.env.NODE_ENV': `"${process.env.NODE_ENV||"production"}"`
51 | }),
52 | new HappyPack({
53 | loaders: [ 'babel' ]
54 | }),
55 | ],
56 |
57 |
58 | cssLoader: {
59 | modules: true
60 | },
61 |
62 | postcss : [
63 | cssimport({ path: `${__dirname}/src/app` }),
64 | cssnext()
65 | ]
66 | };
67 |
--------------------------------------------------------------------------------
/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import isBrowser from 'is-in-browser';
4 | import {decode} from '@tuxsudo/b64';
5 | import { routerReducer as routing } from 'react-router-redux';
6 |
7 | import {ALLOW_REDUX_DEV_TOOLS} from './env';
8 |
9 | import system, * as fromSystem from './reducers/system';
10 | import nav, * as fromSiteNav from './reducers/site-nav';
11 | import pageMeta, * as fromPageMeta from './reducers/page-meta';
12 |
13 | // create the master reducer
14 | const rootReducer = combineReducers({nav, system, routing, pageMeta});
15 |
16 |
17 | // Reexport scoped selectors here:
18 | export const selectSiteNav = (state) => (
19 | fromSiteNav.selectSiteNav(state.nav)
20 | );
21 |
22 | export const selectHTTPResponseCode = (state) => (
23 | fromSystem.selectHTTPResponseCode(state.system)
24 | );
25 |
26 | export const selectAllApplicationErrors = (state) => (
27 | fromSystem.selectAllApplicationErrors(state.system)
28 | );
29 |
30 | export const selectApplicationError = (state, id) => (
31 | fromSystem.selectApplicationError(state.system, id)
32 | );
33 |
34 | export const selectPageMeta = (state) => (
35 | fromPageMeta.selectPageMeta(state.pageMeta)
36 | );
37 |
38 | export const selectPageTitle = (state) => (
39 | fromPageMeta.selectPageTitle(state.pageMeta)
40 | );
41 |
42 | export const selectMetaTags = (state) => (
43 | fromPageMeta.selectMetaTags(state.pageMeta)
44 | );
45 |
46 |
47 |
48 | // determine initial state
49 | const initialState = isBrowser && window.__INITIAL_STATE__
50 | ? JSON.parse( decode(window.__INITIAL_STATE__) )
51 | : {};
52 |
53 | const reduxMiddleware = compose(
54 | applyMiddleware(thunk),
55 | isBrowser && ALLOW_REDUX_DEV_TOOLS==="1" && typeof window.devToolsExtension !== "undefined"
56 | ? window.devToolsExtension()
57 | : f => f
58 | );
59 |
60 | // export a store creator factory with initial state if present...
61 | export default () => createStore( rootReducer, initialState, reduxMiddleware );
62 |
--------------------------------------------------------------------------------
/src/app/reducers/__tests__/system.spec.js:
--------------------------------------------------------------------------------
1 | import reducer, {
2 | httpResponse,
3 | errors,
4 | working,
5 | selectHTTPResponseCode,
6 | selectAllApplicationErrors,
7 | selectApplicationError,
8 | selectSystemWorking,
9 | RECEIVE_HTTP_RESPONSE_CODE,
10 | RECEIVE_HTTP_RESPONSE_CODE_RESET,
11 | RECEIVE_APPLICATION_ERROR_RESET,
12 | RECEIVE_APPLICATION_ERROR,
13 | RECEIVE_APPLICATION_ERROR_REMOVAL,
14 | RECEIVE_LOADING_START,
15 | RECEIVE_LOADING_END
16 | } from '../system';
17 |
18 | test('Reducer: system', () => {
19 |
20 | //httpResponse
21 | expect(httpResponse(undefined, {})).toEqual(200)
22 | expect(httpResponse(404, {type: RECEIVE_HTTP_RESPONSE_CODE, payload: 304})).toBe(304)
23 | expect(httpResponse(500, {type: RECEIVE_HTTP_RESPONSE_CODE_RESET})).toBe(200)
24 |
25 |
26 | //errors
27 | const error = {
28 | id:1,
29 | title: 'shucks',
30 | details: 'it broke',
31 | date: new Date()
32 | };
33 |
34 | expect(errors(undefined,{})).toEqual({})
35 | expect(errors(error, {})).toEqual(error)
36 | expect(errors(error, {type:RECEIVE_APPLICATION_ERROR_RESET})).toEqual({})
37 | expect(errors(undefined, {type:RECEIVE_APPLICATION_ERROR, payload:error})).toEqual({1: error})
38 | expect(errors({a:1}, {type:RECEIVE_APPLICATION_ERROR, payload:error})).toEqual({a:1, 1: error})
39 | expect(errors({a:{}, b:{}}, {type:RECEIVE_APPLICATION_ERROR_REMOVAL, payload:{id:'b'}})).toEqual({a:{}})
40 |
41 |
42 | //working
43 | expect(working(undefined,{})).toEqual(false)
44 | expect(working(false, {})).toBe(false)
45 | expect(working(false, {type:RECEIVE_LOADING_START})).toBe(true)
46 | expect(working(true, {type:RECEIVE_LOADING_END})).toBe(false)
47 |
48 | //selectors
49 | const state = {
50 | httpResponse:200,
51 | errors:{
52 | 1: {title: 'a error'},
53 | 2: {title: 'rorre a'}
54 | },
55 | working: false
56 | }
57 | expect(selectHTTPResponseCode(state)).toBe(200)
58 | expect(selectAllApplicationErrors(state)).toEqual([{"title": "a error"}, {"title": "rorre a"}])
59 | expect(selectSystemWorking(state)).toBe(false)
60 | })
--------------------------------------------------------------------------------
/src/app/components/Hero/Hero.story.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { storiesOf, action } from '@kadira/storybook';
3 | import Hero from './Hero';
4 | import imgSrc from './slc.jpg';
5 |
6 |
7 | storiesOf('Hero', module)
8 |
9 | .add('basic', () => ( ))
10 |
11 | .add('subtitled', () => (
12 |
16 | ))
17 |
18 | .add('background', () => (
19 |
24 | ))
25 |
26 | .add('overlay-light', () => (
27 |
33 | ))
34 |
35 | .add('overlay', () => (
36 |
42 | ))
43 |
44 | .add('overlay-dark', () => (
45 |
51 | ))
52 |
53 | .add('onClick', () => (
54 |
61 | ))
62 |
63 | .add('href', () => (
64 |
71 | ));
72 |
--------------------------------------------------------------------------------
/src/app/_server.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import docTemplate from './HTML.js';
3 | import { renderToString } from 'react-dom/server';
4 | import { match, RouterContext } from 'react-router';
5 | import { Provider } from 'react-redux';
6 | import Helmet from 'react-helmet';
7 | import {encode} from '@tuxsudo/b64';
8 | import routes from './routes.js';
9 | import getStore from './store.js';
10 | import { minify } from 'html-minifier';
11 | import { resolve } from './hocs/ss-resolve';
12 | import {selectHTTPResponseCode} from './store.js';
13 |
14 | import * as env from './env.js';
15 |
16 |
17 | export default (req, res, next) => {
18 | match({ routes, location: req.url }, (err, redirect, props) => {
19 |
20 | if (err) {
21 | return next(err);
22 |
23 | } else if (redirect) {
24 | res.redirect(redirect.pathname + redirect.search)
25 |
26 | } else if (props) {
27 | const store = getStore();
28 |
29 | resolve(props, store)
30 | .then(() => {
31 | const initialState = store.getState();
32 | const httpStatus = selectHTTPResponseCode(initialState);
33 | const opaqueStateString = encode(JSON.stringify(initialState));
34 |
35 | const content = renderToString(
36 |
37 |
38 |
39 | );
40 |
41 | res.status(httpStatus).send(
42 | minify(
43 | docTemplate({
44 | ...(Helmet.rewind()),
45 | content,
46 | initialState: opaqueStateString,
47 | env,
48 | base_path: env.APP_WEB_BASE_PATH
49 | }),
50 | { collapseWhitespace: true, removeAttributeQuotes: true }
51 | )
52 | );
53 | }).catch(next);
54 |
55 | } else {
56 | res.status(404).send('Not Found')
57 | }
58 | })
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/reducers/system.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 |
3 | export const RECEIVE_HTTP_RESPONSE_CODE = "RECEIVE_HTTP_RESPONSE_CODE";
4 | export const RECEIVE_HTTP_RESPONSE_CODE_RESET = "RECEIVE_HTTP_RESPONSE_CODE_RESET";
5 |
6 | export const RECEIVE_APPLICATION_ERROR_RESET = "RECEIVE_APPLICATION_ERROR_RESET";
7 | export const RECEIVE_APPLICATION_ERROR = "RECEIVE_APPLICATION_ERROR";
8 | export const RECEIVE_APPLICATION_ERROR_REMOVAL = "RECEIVE_APPLICATION_ERROR_REMOVAL";
9 |
10 | export const RECEIVE_LOADING_START = "RECEIVE_LOADING_START";
11 | export const RECEIVE_LOADING_END = "RECEIVE_LOADING_END";
12 |
13 |
14 | export const httpResponse = (state = 200, { type, payload: httpCode } ) => {
15 | switch(type) {
16 |
17 | case RECEIVE_HTTP_RESPONSE_CODE: {
18 | return httpCode;
19 | }
20 |
21 | case RECEIVE_HTTP_RESPONSE_CODE_RESET: {
22 | return 200;
23 | }
24 |
25 | default: {
26 | return state;
27 | }
28 | }
29 | };
30 |
31 | export const errors = (state = {}, {type, payload}) => {
32 | switch(type) {
33 | case RECEIVE_APPLICATION_ERROR_RESET: {
34 | return {};
35 | }
36 |
37 | case RECEIVE_APPLICATION_ERROR: {
38 | const {id, title, details, date} = payload;
39 | return {
40 | ...state,
41 | [id]: {id, title, details, date}
42 | };
43 | }
44 |
45 | case RECEIVE_APPLICATION_ERROR_REMOVAL: {
46 | const { id } = payload;
47 | const newState = {...state};
48 | delete newState[id];
49 | return newState;
50 | }
51 |
52 | default:
53 | return state;
54 | }
55 | }
56 |
57 | export const working = (state = false, {type}) => {
58 | switch(type) {
59 | case RECEIVE_LOADING_START: {
60 | return true;
61 | }
62 |
63 | case RECEIVE_LOADING_END: {
64 | return false;
65 | }
66 |
67 | default:
68 | return state;
69 | }
70 | };
71 |
72 |
73 |
74 | export default combineReducers({httpResponse, errors, working});
75 |
76 |
77 | /*SELECTOR(S)*/
78 | export const selectHTTPResponseCode = (state) => state.httpResponse;
79 |
80 | export const selectAllApplicationErrors = (state) => (
81 | Object.keys(state.errors)
82 | .map((key) => state.errors[key])
83 | );
84 |
85 | export const selectApplicationError = (state, id) => (
86 | state.errors[id]
87 | );
88 |
89 | export const selectSystemWorking = (state) => state.working;
90 |
--------------------------------------------------------------------------------
/.storybook/whitelister.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const whitelistresolver = (path, ignore, depth) => {
4 |
5 | // safeguard to avoid a forever crawl
6 | const maxDepth = depth || 40;
7 |
8 | // create an ignore filter. Provided an array, it will return a function
9 | // that checks if an item is in that array.
10 | // useful with [].filter
11 | const createIgnoreFilter = (exclusions) => (item) => exclusions.indexOf(item)===-1;
12 |
13 | // remove duplicates from array
14 | const isUnique = (item, i, arr) => arr.indexOf(item)===i;
15 |
16 | // flatten array deeply
17 | const flatten = (l, c) => l.concat(Array.isArray(c) ? flatten(c) : c);
18 |
19 | // flatten, remove falsey values, remove duplicates
20 | const normalizeArray = (arr) => (arr||[])
21 | .reduce(flatten, [])
22 | .filter(x=>x)
23 | .filter(isUnique);
24 |
25 | const packages = Object.keys(require(path).dependencies||{})
26 | .filter( createIgnoreFilter(normalizeArray(ignore)) );
27 |
28 |
29 | // grab the denormalized list of child deps (may be nested with dupes)
30 | const uncleanChildren = (
31 | packages.map(d => Object.keys(require(`${d}/package.json`).dependencies||{}))
32 | );
33 |
34 | // create a new ignore list from parent
35 | const childIgnoreList = normalizeArray( [].concat(ignore).concat(packages));
36 |
37 | // flatten, remove dupes and falseys, and remove items that appear in new
38 | // ignore list.
39 | const childDeps = normalizeArray(uncleanChildren)
40 | .filter( createIgnoreFilter(childIgnoreList));
41 |
42 |
43 | var descendents = [];
44 | if(maxDepth>0) {
45 | // create a new ignore list from original ignore items, root packages and child packages
46 | const deepIgnoreList = normalizeArray([]
47 | .concat(ignore)
48 | .concat(packages)
49 | .concat(childDeps)
50 | );
51 |
52 | // recursive crawl all childrens deps ignore anything that was already found
53 | // or is on the original ignore list.
54 | // return a denormalized list of deps
55 | const uncleanDescendents = childDeps
56 | .map(desc => whitelistresolver(`${desc}/package.json`, deepIgnoreList, maxDepth-1));
57 |
58 | // flatten, dedupe and remove falseys then remove previously found items or things in original ignore list.
59 | descendents = normalizeArray(uncleanDescendents)
60 | .filter(createIgnoreFilter(deepIgnoreList));
61 | }
62 |
63 |
64 | return normalizeArray([]
65 | .concat(packages)
66 | .concat(childDeps)
67 | .concat(descendents)
68 | );
69 |
70 |
71 | }
72 |
73 |
74 | module.exports = (ignore) => {
75 |
76 | const whitelist = whitelistresolver('../package.json', ignore || ['react']);
77 |
78 | // ignore all packages in node_modules, except for things in the whitelist
79 | return whitelist.length
80 | ? new RegExp(`node_modules.(?!${whitelist.join("|")})`)
81 | : path.resolve('node_modules/');
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/mdjasper/react-starter.svg?branch=master)
2 |
3 | [](https://coveralls.io/github/mdjasper/react-starter?branch=master)
4 |
5 | # React/Redux Project Starter
6 |
7 | A project starter for universalmorphic React/Redux apps
8 |
9 |
10 | ## Features
11 |
12 | * [React](https://facebook.github.io/react/): THE component-based view library.
13 | * [Redux](http://redux.js.org/): application state management
14 | * [React-Redux Official Bindings](https://github.com/reactjs/react-redux): remove the boilerplate code when connecting React to Redux
15 | * [React Router](https://github.com/reactjs/react-router): server/client side application router
16 | * [Redux Thunk](https://github.com/gaearon/redux-thunk): easier async and sequential actions.
17 | * [React Helmet](https://github.com/nfl/react-helmet): title and meta tags FTW
18 | * [ExpressJS](http://expressjs.com/): server-side app framework.
19 | * [WebPack](https://webpack.github.io/): module bundler.
20 | * [PostCSS](https://github.com/postcss/postcss): CSS transformations via JS
21 | * [CSS Modules](https://github.com/css-modules/css-modules): private name spaces for css classes.
22 | * [CSSNext](http://cssnext.io/): future CSS today.
23 | * [React StoryBook](https://github.com/kadirahq/react-storybook): a component authoring sandbox. also component functional testing .
24 | * [Jest](https://facebook.github.io/jest/): unit testing and coverage reporting
25 | * [Eslint](http://eslint.org/): JS linting.
26 |
27 |
28 | ## Development
29 |
30 | 1. first, run `npm i` to install dependencies
31 | 1. to start the component dev environment, run `npm run components`
32 | 1. to start the application dev environment, run `npm run dev`
33 | 1. to lint the JS, run `npm run lint`
34 | 1. build the production app via `npm run build`
35 | 1. start the app via `npm start`
36 |
37 |
38 | ## Files
39 |
40 | 1. The React / Redux app is found in [src/app](./src/app)
41 | 1. The production Express.js server is found in [src/server](./src/server)
42 | 1. Component stories (for the component dev environment) are found in [.storybook/config.js](./.storybook/config.js)
43 |
44 | ## FAQ
45 |
46 | 1. Do I need a particular version of `npm`?
47 | - Please use `npm` version 3 or higher.
48 | 1. Why do you have `package.json` files in your Component directories?
49 | - Placing a `package.json` file with a proper `main` property allows you to `import` that code by referencing only the parent folder. Example: `import LandingView from '../views/LandingView';`. This gives you the flexibility of refactoring the Component without changing the consuming `import`s.
50 | 1. My editor/linter claims the `package.json` files in the Component directories are missing the `name` and `version` properties.
51 | - According to the specification, `name` and `version` are both required properties of a `package.json` file. However, they are not necessary in this context because we are not publishing the Components separate from the project. We chose to have minimal `package.json` files. Please feel free to add the missing values if this bothers you or your linter enough that you or your linter can't get past it.
52 | 1. How do I allow node modules to be processed by webpack (for example, be processed by babel)?
53 | - There is a whitelist array in the file `webpack.server.babel.js` which allows packages to be processed by webpack if they are included. To add a package called `my-foo-bar`, the array would be `whitelist: ['normalize.css', 'my-foo-bar]`. To include all packages under a certain namespace, you can use a regular expression test. For example, to add all packages under `@foo`, your array would be `whitelist: ['normalize.css', /^[@]foo/]`
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-starter",
3 | "version": "1.0.1",
4 | "description": "An Isomorphic React Starter",
5 | "main": "index.js",
6 | "scripts": {
7 | "predev": "npm run build",
8 | "dev": "npm-run-all -p dev:*",
9 | "build": "npm-run-all -p build:*",
10 | "start": "node dist/server.js",
11 | "test": "jest --coverage",
12 | "test:watch": "jest --watch",
13 | "test:coverage": "jest --no-cache --coverage",
14 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls",
15 | "lint": "eslint src",
16 | "components": "start-storybook -p 9999",
17 | "create:component": "componentinator",
18 | "create:view": "componentinator type=view",
19 | "create:container": "componentinator type=container",
20 | "build:client": "better-npm-run build:client",
21 | "build:server": "better-npm-run build:server",
22 | "dev:client": "better-npm-run dev:client",
23 | "dev:server": "better-npm-run dev:server"
24 | },
25 | "betterScripts": {
26 | "dev:client": {
27 | "command": "webpack-dev-server -d --quiet --no-info --hot --inline --history-api-fallback --config webpack.client.babel.js",
28 | "env": {
29 | "NODE_ENV": "development"
30 | }
31 | },
32 | "dev:server": {
33 | "command": "node dist/server.js",
34 | "env": {
35 | "NODE_ENV": "development"
36 | }
37 | },
38 | "build:client": {
39 | "command": "webpack --quiet --no-info --config webpack.client.babel.js",
40 | "env": {
41 | "NODE_ENV": "production"
42 | }
43 | },
44 | "build:server": {
45 | "command": "webpack --quiet --no-info --config webpack.server.babel.js",
46 | "env": {
47 | "NODE_ENV": "production"
48 | }
49 | }
50 | },
51 | "author": "Jared Anderson",
52 | "license": "MIT",
53 | "devDependencies": {
54 | "@kadira/storybook": "^2.35.3",
55 | "autoprefixer": "^7.2.5",
56 | "babel-cli": "^6.5.1",
57 | "babel-core": "^6.2.1",
58 | "babel-eslint": "^8.2.1",
59 | "babel-jest": "^22.1.0",
60 | "babel-loader": "^7.1.2",
61 | "babel-plugin-react-require": "^3.0.0",
62 | "babel-preset-es2015": "^6.24.1",
63 | "babel-preset-latest": "^6.16.0",
64 | "babel-preset-react": "^6.24.1",
65 | "babel-preset-stage-2": "^6.5.0",
66 | "css-loader": "^0.26.4",
67 | "enzyme": "^2.9.1",
68 | "eslint": "^4.16.0",
69 | "eslint-config-react-app": "^2.1.0",
70 | "eslint-plugin-flowtype": "^2.30.0",
71 | "eslint-plugin-import": "^2.2.0",
72 | "eslint-plugin-jsx-a11y": "^6.0.3",
73 | "eslint-plugin-react": "^7.5.1",
74 | "extract-text-webpack-plugin": "^1.0.1",
75 | "file-loader": "^1.1.6",
76 | "happypack": "^4.0.1",
77 | "identity-obj-proxy": "^3.0.0",
78 | "jest": "^21.2.1",
79 | "json-loader": "^0.5.4",
80 | "normalize.css": "^7.0.0",
81 | "postcss-cssnext": "^2.4.0",
82 | "postcss-import": "^9.1.0",
83 | "postcss-loader": "^1.2.2",
84 | "react-addons-test-utils": "^15.0.1",
85 | "react-component-inator": "^1.0.2",
86 | "react-test-renderer": "^15.6.1",
87 | "sinon": "^4.2.1",
88 | "style-loader": "^0.19.1",
89 | "webpack": "^1.12.9",
90 | "webpack-babel-jest": "^1.0.4",
91 | "webpack-dev-server": "^1.14.0",
92 | "webpack-node-externals": "^1.2.0"
93 | },
94 | "dependencies": {
95 | "@tuxsudo/b64": "^1.0.1",
96 | "better-npm-run": "0.1.0",
97 | "body-parser": "^1.17.1",
98 | "compression": "^1.6.1",
99 | "cors": "^2.7.1",
100 | "coveralls": "^3.0.0",
101 | "dotenv": "^4.0.0",
102 | "express": "^4.13.4",
103 | "helmet": "^3.4.0",
104 | "html-minifier": "^3.3.0",
105 | "is-in-browser": "^1.0.2",
106 | "isomorphic-fetch": "^2.2.1",
107 | "join-classnames": "^1.0.0",
108 | "npm-run-all": "^4.0.1",
109 | "react": "^15.0.1",
110 | "react-dom": "^15.0.1",
111 | "react-helmet": "^5.0.3",
112 | "react-redux": "^5.0.2",
113 | "react-router": "^3.0.2",
114 | "react-router-redux": "^4.0.7",
115 | "redux": "^3.4.0",
116 | "redux-thunk": "^2.0.1",
117 | "string-hash": "^1.1.0"
118 | }
119 | }
120 |
--------------------------------------------------------------------------------