├── .env
├── .eslintignore
├── api
├── actions
│ ├── widget
│ │ ├── index.js
│ │ ├── update.js
│ │ └── load.js
│ ├── loadAuth.js
│ ├── login.js
│ ├── index.js
│ ├── loadInfo.js
│ └── logout.js
├── utils
│ └── url.js
├── __tests__
│ └── api-test.js
└── api.js
├── static
├── logo.jpg
├── favicon.ico
└── favicon.png
├── src
├── containers
│ ├── Home
│ │ ├── logo.png
│ │ ├── Home.scss
│ │ └── Home.js
│ ├── About
│ │ ├── kitten.jpg
│ │ └── About.js
│ ├── Chat
│ │ ├── Chat.scss
│ │ └── Chat.js
│ ├── Login
│ │ ├── Login.scss
│ │ └── Login.js
│ ├── NotFound
│ │ └── NotFound.js
│ ├── DevTools
│ │ └── DevTools.js
│ ├── index.js
│ ├── Uploader
│ │ └── Uploader.js
│ ├── App
│ │ ├── App.scss
│ │ └── App.js
│ ├── Widgets
│ │ ├── Widgets.scss
│ │ └── Widgets.js
│ ├── LoginSuccess
│ │ └── LoginSuccess.js
│ └── Survey
│ │ └── Survey.js
├── components
│ ├── InfoBar
│ │ ├── InfoBar.scss
│ │ └── InfoBar.js
│ ├── SurveyForm
│ │ ├── surveyValidation.js
│ │ ├── SurveyForm.scss
│ │ └── SurveyForm.js
│ ├── WidgetForm
│ │ ├── widgetValidation.js
│ │ └── WidgetForm.js
│ ├── index.js
│ ├── MiniInfoBar
│ │ └── MiniInfoBar.js
│ ├── CounterButton
│ │ └── CounterButton.js
│ ├── GithubButton
│ │ └── GithubButton.js
│ └── __tests__
│ │ └── InfoBar-test.js
├── theme
│ ├── font-awesome.config.less
│ ├── bootstrap.overrides.scss
│ ├── bootstrap.config.prod.js
│ ├── font-awesome.config.prod.js
│ ├── font-awesome.config.js
│ ├── variables.scss
│ └── bootstrap.config.js
├── helpers
│ ├── getStatusFromRoutes.js
│ ├── getDataDependencies.js
│ ├── makeRouteHooksSafe.js
│ ├── ApiClient.js
│ ├── __tests__
│ │ ├── getStatusFromRoutes-test.js
│ │ ├── getDataDependencies-test.js
│ │ └── makeRouteHooksSafe-test.js
│ └── Html.js
├── redux
│ ├── modules
│ │ ├── counter.js
│ │ ├── reducer.js
│ │ ├── info.js
│ │ ├── auth.js
│ │ └── widgets.js
│ ├── middleware
│ │ ├── clientMiddleware.js
│ │ └── transitionMiddleware.js
│ └── create.js
├── config.js
├── routes.js
├── utils
│ └── validation.js
├── client.js
└── server.js
├── tests.webpack.js
├── .gitignore
├── docs
├── Ducks.md
└── InlineStyles.md
├── .editorconfig
├── .travis.yml
├── bin
├── api.js
└── server.js
├── server.babel.js
├── .babelrc
├── app.json
├── webpack
├── webpack-dev-server.js
├── prod.config.js
├── webpack-isomorphic-tools.js
└── dev.config.js
├── LICENSE
├── .eslintrc
├── karma.conf.js
├── package.json
└── README.md
/.env:
--------------------------------------------------------------------------------
1 | NODE_PATH=./src
2 | NODE_ENV=production
3 | PORT=80
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | webpack/*
2 | karma.conf.js
3 | tests.webpack.js
4 |
--------------------------------------------------------------------------------
/api/actions/widget/index.js:
--------------------------------------------------------------------------------
1 | export update from './update';
2 | export load from './load';
3 |
--------------------------------------------------------------------------------
/static/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reggi/react-shopify-app/HEAD/static/logo.jpg
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reggi/react-shopify-app/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reggi/react-shopify-app/HEAD/static/favicon.png
--------------------------------------------------------------------------------
/src/containers/Home/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reggi/react-shopify-app/HEAD/src/containers/Home/logo.png
--------------------------------------------------------------------------------
/tests.webpack.js:
--------------------------------------------------------------------------------
1 | var context = require.context('./src', true, /-test\.js$/);
2 | context.keys().forEach(context);
3 |
--------------------------------------------------------------------------------
/src/containers/About/kitten.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reggi/react-shopify-app/HEAD/src/containers/About/kitten.jpg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules/
3 | dist/
4 | *.iml
5 | webpack-assets.json
6 | webpack-stats.json
7 | npm-debug.log
8 |
--------------------------------------------------------------------------------
/api/actions/loadAuth.js:
--------------------------------------------------------------------------------
1 | export default function loadAuth(req) {
2 | return Promise.resolve(req.session.user || null);
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/InfoBar/InfoBar.scss:
--------------------------------------------------------------------------------
1 | .infoBar {
2 | font-variant: italics;
3 | }
4 |
5 | .time {
6 | margin: 0 30px;
7 | }
8 |
--------------------------------------------------------------------------------
/docs/Ducks.md:
--------------------------------------------------------------------------------
1 | This document has found [another, hopefully permanent, home](https://github.com/erikras/ducks-modular-redux).
2 |
3 | Quack.
4 |
--------------------------------------------------------------------------------
/src/theme/font-awesome.config.less:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration file for font-awesome-webpack
3 | *
4 | */
5 |
6 | // Example:
7 | // @fa-border-color: #ddd;
8 |
--------------------------------------------------------------------------------
/api/actions/login.js:
--------------------------------------------------------------------------------
1 | export default function login(req) {
2 | const user = {
3 | name: req.body.name
4 | };
5 | req.session.user = user;
6 | return Promise.resolve(user);
7 | }
8 |
--------------------------------------------------------------------------------
/api/actions/index.js:
--------------------------------------------------------------------------------
1 | export loadInfo from './loadInfo';
2 | export loadAuth from './loadAuth';
3 | export login from './login';
4 | export logout from './logout';
5 | export * as widget from './widget/index';
6 |
--------------------------------------------------------------------------------
/src/theme/bootstrap.overrides.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Override Bootstrap styles that you can't modify via variables here.
3 | *
4 | */
5 |
6 | .navbar-brand {
7 | position: relative;
8 | padding-left: 50px;
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | end_of_line = lf
4 | indent_size = 2
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 |
8 | [*.md]
9 | max_line_length = 0
10 | trim_trailing_whitespace = false
11 |
--------------------------------------------------------------------------------
/api/actions/loadInfo.js:
--------------------------------------------------------------------------------
1 | export default function loadInfo() {
2 | return new Promise((resolve) => {
3 | resolve({
4 | message: 'This came from the api server',
5 | time: Date.now()
6 | });
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/api/actions/logout.js:
--------------------------------------------------------------------------------
1 | export default function logout(req) {
2 | return new Promise((resolve) => {
3 | req.session.destroy(() => {
4 | req.session = null;
5 | return resolve(null);
6 | });
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/src/containers/Chat/Chat.scss:
--------------------------------------------------------------------------------
1 | .chat {
2 | input {
3 | padding: 5px 10px;
4 | border-radius: 5px;
5 | border: 1px solid #ccc;
6 | }
7 | form {
8 | margin: 30px 0;
9 | :global(.btn) {
10 | margin-left: 10px;
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/containers/Login/Login.scss:
--------------------------------------------------------------------------------
1 | .loginPage {
2 | input {
3 | padding: 5px 10px;
4 | border-radius: 5px;
5 | border: 1px solid #ccc;
6 | }
7 | form {
8 | margin: 30px 0;
9 | :global(.btn) {
10 | margin-left: 10px;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "0.12"
5 | - "4.0"
6 | - "4"
7 |
8 | sudo: false
9 |
10 | before_script:
11 | - export DISPLAY=:99.0
12 | - sh -e /etc/init.d/xvfb start
13 |
14 | script:
15 | - npm run lint
16 | - npm test
17 | - npm run test-node
18 |
--------------------------------------------------------------------------------
/src/theme/bootstrap.config.prod.js:
--------------------------------------------------------------------------------
1 | const bootstrapConfig = require('./bootstrap.config.js');
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 | bootstrapConfig.styleLoader = ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader');
4 | module.exports = bootstrapConfig;
5 |
6 |
--------------------------------------------------------------------------------
/src/theme/font-awesome.config.prod.js:
--------------------------------------------------------------------------------
1 | const fontAwesomeConfig = require('./font-awesome.config.js');
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 | fontAwesomeConfig.styleLoader = ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader');
4 | module.exports = fontAwesomeConfig;
5 |
6 |
--------------------------------------------------------------------------------
/src/helpers/getStatusFromRoutes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Return the status code from the last matched route with a status property.
3 | *
4 | * @param matchedRoutes
5 | * @returns {Number|null}
6 | */
7 | export default (matchedRoutes) => {
8 | return matchedRoutes.reduce((prev, cur) => cur.status || prev, null);
9 | };
10 |
--------------------------------------------------------------------------------
/bin/api.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | if (process.env.NODE_ENV !== 'production') {
3 | if (!require('piping')({
4 | hook: true,
5 | ignore: /(\/\.|~$|\.json$)/i
6 | })) {
7 | return;
8 | }
9 | }
10 | require('../server.babel'); // babel registration (runtime transpilation for node)
11 | require('../api/api');
12 |
--------------------------------------------------------------------------------
/src/containers/NotFound/NotFound.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | export default class NotFound extends Component {
4 | render() {
5 | return (
6 |
7 |
Doh! 404!
8 |
These are not the droids you are looking for!
9 |
10 | );
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/theme/font-awesome.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Configuration file for font-awesome-webpack
3 | *
4 | * In order to keep the bundle size low in production,
5 | * disable components you don't use.
6 | *
7 | */
8 |
9 | module.exports = {
10 | styles: {
11 | mixins: true,
12 | core: true,
13 | icons: true,
14 | larger: true,
15 | path: true,
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/server.babel.js:
--------------------------------------------------------------------------------
1 | // enable runtime transpilation to use ES6/7 in node
2 |
3 | var fs = require('fs');
4 |
5 | var babelrc = fs.readFileSync('./.babelrc');
6 | var config;
7 |
8 | try {
9 | config = JSON.parse(babelrc);
10 | } catch (err) {
11 | console.error('==> ERROR: Error parsing your .babelrc.');
12 | console.error(err);
13 | }
14 |
15 | require('babel-core/register')(config);
16 |
--------------------------------------------------------------------------------
/src/containers/DevTools/DevTools.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createDevTools } from 'redux-devtools';
3 | import LogMonitor from 'redux-devtools-log-monitor';
4 | import DockMonitor from 'redux-devtools-dock-monitor';
5 |
6 | export default createDevTools(
7 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/components/SurveyForm/surveyValidation.js:
--------------------------------------------------------------------------------
1 | import memoize from 'lru-memoize';
2 | import {createValidator, required, maxLength, email} from 'utils/validation';
3 |
4 | const surveyValidation = createValidator({
5 | name: [required, maxLength(10)],
6 | email: [required, email],
7 | occupation: maxLength(20) // single rules don't have to be in an array
8 | });
9 | export default memoize(10)(surveyValidation);
10 |
--------------------------------------------------------------------------------
/src/components/WidgetForm/widgetValidation.js:
--------------------------------------------------------------------------------
1 | import {createValidator, required, maxLength, integer, oneOf} from 'utils/validation';
2 |
3 | export const colors = ['Blue', 'Fuchsia', 'Green', 'Orange', 'Red', 'Taupe'];
4 |
5 | const widgetValidation = createValidator({
6 | color: [required, oneOf(colors)],
7 | sprocketCount: [required, integer],
8 | owner: [required, maxLength(30)]
9 | });
10 | export default widgetValidation;
11 |
--------------------------------------------------------------------------------
/src/containers/index.js:
--------------------------------------------------------------------------------
1 | export App from './App/App';
2 | export Chat from './Chat/Chat';
3 | export Home from './Home/Home';
4 | export Widgets from './Widgets/Widgets';
5 | export About from './About/About';
6 | export Login from './Login/Login';
7 | export LoginSuccess from './LoginSuccess/LoginSuccess';
8 | export Survey from './Survey/Survey';
9 | export NotFound from './NotFound/NotFound';
10 | export Uploader from './Uploader/Uploader';
11 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Point of contact for component modules
3 | *
4 | * ie: import { CounterButton, InfoBar } from 'components';
5 | *
6 | */
7 |
8 | export CounterButton from './CounterButton/CounterButton';
9 | export GithubButton from './GithubButton/GithubButton';
10 | export InfoBar from './InfoBar/InfoBar';
11 | export MiniInfoBar from './MiniInfoBar/MiniInfoBar';
12 | export SurveyForm from './SurveyForm/SurveyForm';
13 | export WidgetForm from './WidgetForm/WidgetForm';
14 |
--------------------------------------------------------------------------------
/src/containers/Uploader/Uploader.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Dropzone from 'react-dropzone';
3 |
4 | export default class Reggi extends Component {
5 | onDrop(files) {
6 | console.log('Received files: ', files);
7 | }
8 | render() {
9 | return (
10 |
11 |
12 | Try dropping some files here, or click to select files to upload.
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/redux/modules/counter.js:
--------------------------------------------------------------------------------
1 | const INCREMENT = 'redux-example/counter/INCREMENT';
2 |
3 | const initialState = {
4 | count: 0
5 | };
6 |
7 | export default function reducer(state = initialState, action = {}) {
8 | switch (action.type) {
9 | case INCREMENT:
10 | const {count} = state;
11 | return {
12 | count: count + 1
13 | };
14 | default:
15 | return state;
16 | }
17 | }
18 |
19 | export function increment() {
20 | return {
21 | type: INCREMENT
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/containers/App/App.scss:
--------------------------------------------------------------------------------
1 | .app {
2 | .brand {
3 | position: absolute;
4 | $size: 40px;
5 | top: 5px;
6 | left: 5px;
7 | display: inline-block;
8 | background: #2d2d2d url('../Home/logo.png') no-repeat center center;
9 | width: $size;
10 | height: $size;
11 | background-size: 80%;
12 | margin: 0 10px 0 0;
13 | border-radius: $size / 2;
14 | }
15 | nav :global(.fa) {
16 | font-size: 2em;
17 | line-height: 20px;
18 | }
19 | }
20 | .appContent {
21 | margin: 50px 0; // for fixed navbar
22 | }
23 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "optional": "runtime",
4 | "loose": "all",
5 | "plugins": [
6 | "typecheck"
7 | ],
8 | "env": {
9 | "development": {
10 | "plugins": [
11 | "react-transform"
12 | ],
13 | "extra": {
14 | "react-transform": {
15 | "transforms": [{
16 | "transform": "react-transform-catch-errors",
17 | "imports": [
18 | "react",
19 | "redbox-react"
20 | ]
21 | }]
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-universal-hot-example",
3 | "description": "Example of an isomorphic (universal) webapp using react redux and hot reloading",
4 | "repository": "https://github.com/erikras/react-redux-universal-hot-example",
5 | "logo": "http://node-js-sample.herokuapp.com/node.svg",
6 | "keywords": [
7 | "react",
8 | "isomorphic",
9 | "universal",
10 | "webpack",
11 | "express",
12 | "hot reloading",
13 | "react-hot-reloader",
14 | "redux",
15 | "starter",
16 | "boilerplate",
17 | "babel"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/theme/variables.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Define scss variables here.
3 | *
4 | * Available options for Bootstrap:
5 | * http://getbootstrap.com/customize/
6 | *
7 | */
8 |
9 | // Custom Colors
10 | $cyan: #33e0ff;
11 | $humility: #777;
12 |
13 | // Bootstrap Variables
14 | $brand-primary: darken(#428bca, 6.5%);
15 | $brand-secondary: #e25139;
16 | $brand-success: #5cb85c;
17 | $brand-warning: #f0ad4e;
18 | $brand-danger: #d9534f;
19 | $brand-info: #5bc0de;
20 |
21 | $text-color: #333;
22 |
23 | $font-size-base: 14px;
24 | $font-family-sans-serif: "Helvetica Neue", Helvetica, sans-serif;
25 |
--------------------------------------------------------------------------------
/src/components/MiniInfoBar/MiniInfoBar.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 |
4 | @connect(state => ({ time: state.info.data.time }))
5 | export default class MiniInfoBar extends Component {
6 | static propTypes = {
7 | time: PropTypes.number
8 | }
9 |
10 | render() {
11 | const {time} = this.props;
12 | return (
13 |
14 | The info bar was last loaded at
15 | {' '}
16 | {time && new Date(time).toString()}
17 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/redux/modules/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import multireducer from 'multireducer';
3 | import { routerStateReducer } from 'redux-router';
4 |
5 | import auth from './auth';
6 | import counter from './counter';
7 | import {reducer as form} from 'redux-form';
8 | import info from './info';
9 | import widgets from './widgets';
10 |
11 | export default combineReducers({
12 | router: routerStateReducer,
13 | auth,
14 | form,
15 | multireducer: multireducer({
16 | counter1: counter,
17 | counter2: counter,
18 | counter3: counter
19 | }),
20 | info,
21 | widgets
22 | });
23 |
--------------------------------------------------------------------------------
/src/containers/Widgets/Widgets.scss:
--------------------------------------------------------------------------------
1 | .widgets {
2 | .refreshBtn {
3 | margin-left: 20px;
4 | }
5 | .idCol {
6 | width: 5%;
7 | }
8 | .colorCol {
9 | width: 20%;
10 | }
11 | .sprocketsCol {
12 | width: 20%;
13 | text-align: right;
14 | input {
15 | text-align: right;
16 | }
17 | }
18 | .ownerCol {
19 | width: 30%;
20 | }
21 | .buttonCol {
22 | width: 25%;
23 | :global(.btn) {
24 | margin: 0 5px;
25 | }
26 | }
27 | tr.saving {
28 | opacity: 0.8;
29 | :global(.btn) {
30 | &[disabled] {
31 | opacity: 1;
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/helpers/getDataDependencies.js:
--------------------------------------------------------------------------------
1 | const getDataDependency = (component = {}, methodName) => {
2 | return component.WrappedComponent ?
3 | getDataDependency(component.WrappedComponent, methodName) :
4 | component[methodName];
5 | };
6 |
7 | export default (components, getState, dispatch, location, params, deferred) => {
8 | const methodName = deferred ? 'fetchDataDeferred' : 'fetchData';
9 |
10 | return components
11 | .filter((component) => getDataDependency(component, methodName)) // only look at ones with a static fetchData()
12 | .map((component) => getDataDependency(component, methodName)) // pull out fetch data methods
13 | .map(fetchData =>
14 | fetchData(getState, dispatch, location, params)); // call fetch data methods and save promises
15 | };
16 |
--------------------------------------------------------------------------------
/src/redux/middleware/clientMiddleware.js:
--------------------------------------------------------------------------------
1 | export default function clientMiddleware(client) {
2 | return ({dispatch, getState}) => {
3 | return next => action => {
4 | if (typeof action === 'function') {
5 | return action(dispatch, getState);
6 | }
7 |
8 | const { promise, types, ...rest } = action;
9 | if (!promise) {
10 | return next(action);
11 | }
12 |
13 | const [REQUEST, SUCCESS, FAILURE] = types;
14 | next({...rest, type: REQUEST});
15 | return promise(client).then(
16 | (result) => next({...rest, result, type: SUCCESS}),
17 | (error) => next({...rest, error, type: FAILURE})
18 | ).catch((error)=> {
19 | console.error('MIDDLEWARE ERROR:', error);
20 | next({...rest, error, type: FAILURE});
21 | });
22 | };
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/api/actions/widget/update.js:
--------------------------------------------------------------------------------
1 | import load from './load';
2 |
3 | export default function update(req) {
4 | return new Promise((resolve, reject) => {
5 | // write to database
6 | setTimeout(() => {
7 | if (Math.random() < 0.2) {
8 | reject('Oh no! Widget save fails 20% of the time. Try again.');
9 | } else {
10 | const widgets = load(req);
11 | const widget = req.body;
12 | if (widget.color === 'Green') {
13 | reject({
14 | color: 'We do not accept green widgets' // example server-side validation error
15 | });
16 | }
17 | if (widget.id) {
18 | widgets[widget.id - 1] = widget; // id is 1-based. please don't code like this in production! :-)
19 | }
20 | resolve(widget);
21 | }
22 | }, 2000); // simulate async db write
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/SurveyForm/SurveyForm.scss:
--------------------------------------------------------------------------------
1 | .inputGroup {
2 | position: relative;
3 | }
4 |
5 | .flags {
6 | position: absolute;
7 | right: 20px;
8 | top: 7px;
9 | & > * {
10 | margin: 0 2px;
11 | width: 20px;
12 | height: 20px;
13 | border-radius: 20px;
14 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.4);
15 | color: white;
16 | float: right;
17 | text-align: center;
18 | }
19 | .active {
20 | background: linear-gradient(#cc0, #aa0);
21 | color: black;
22 | }
23 | .dirty {
24 | background: linear-gradient(#090, #060);
25 | }
26 | .visited {
27 | background: linear-gradient(#009, #006);
28 | }
29 | .touched {
30 | background: linear-gradient(#099, #066);
31 | }
32 | }
33 |
34 | .radioLabel {
35 | margin: 0 25px 0 5px;
36 | }
37 | .cog {
38 | position: absolute;
39 | left: 0;
40 | top: 10px;
41 | }
42 |
--------------------------------------------------------------------------------
/api/actions/widget/load.js:
--------------------------------------------------------------------------------
1 | const initialWidgets = [
2 | {id: 1, color: 'Red', sprocketCount: 7, owner: 'John'},
3 | {id: 2, color: 'Taupe', sprocketCount: 1, owner: 'George'},
4 | {id: 3, color: 'Green', sprocketCount: 8, owner: 'Ringo'},
5 | {id: 4, color: 'Blue', sprocketCount: 2, owner: 'Paul'}
6 | ];
7 |
8 | export function getWidgets(req) {
9 | let widgets = req.session.widgets;
10 | if (!widgets) {
11 | widgets = initialWidgets;
12 | req.session.widgets = widgets;
13 | }
14 | return widgets;
15 | }
16 |
17 | export default function load(req) {
18 | return new Promise((resolve, reject) => {
19 | // make async call to database
20 | setTimeout(() => {
21 | if (Math.random() < 0.33) {
22 | reject('Widget load fails 33% of the time. You were unlucky.');
23 | } else {
24 | resolve(getWidgets(req));
25 | }
26 | }, 1000); // simulate async load
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/api/utils/url.js:
--------------------------------------------------------------------------------
1 | export function mapUrl(availableActions = {}, url = []) {
2 |
3 | const notFound = {action: null, params: []};
4 |
5 | // test for empty input
6 | if (url.length === 0 || Object.keys(availableActions).length === 0) {
7 | return notFound;
8 | }
9 | /*eslint-disable */
10 | const reducer = (next, current) => {
11 | if (next.action && next.action[current]) {
12 | return {action: next.action[current], params: []}; // go deeper
13 | } else {
14 | if (typeof next.action === 'function') {
15 | return {action: next.action, params: next.params.concat(current)}; // params are found
16 | } else {
17 | return notFound;
18 | }
19 | }
20 | };
21 | /*eslint-enable */
22 |
23 | const actionAndParams = url.reduce(reducer, {action: availableActions, params: []});
24 |
25 | return (typeof actionAndParams.action === 'function') ? actionAndParams : notFound;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/CounterButton/CounterButton.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connectMultireducer} from 'multireducer';
3 | import {increment} from 'redux/modules/counter';
4 |
5 | @connectMultireducer(
6 | state => ({count: state.count}),
7 | {increment})
8 | export default class CounterButton extends Component {
9 | static propTypes = {
10 | count: PropTypes.number,
11 | increment: PropTypes.func.isRequired,
12 | className: PropTypes.string
13 | }
14 |
15 | props = {
16 | className: ''
17 | }
18 |
19 | render() {
20 | const {count, increment} = this.props; // eslint-disable-line no-shadow
21 | let {className} = this.props;
22 | className += ' btn btn-default';
23 | return (
24 |
25 | You have clicked me {count} time{count === 1 ? '' : 's'}.
26 |
27 | );
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/bin/server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('../server.babel'); // babel registration (runtime transpilation for node)
3 | var path = require('path');
4 | var rootDir = path.resolve(__dirname, '..');
5 | /**
6 | * Define isomorphic constants.
7 | */
8 | global.__CLIENT__ = false;
9 | global.__SERVER__ = true;
10 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING
11 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production';
12 |
13 | if (__DEVELOPMENT__) {
14 | if (!require('piping')({
15 | hook: true,
16 | ignore: /(\/\.|~$|\.json|\.scss$)/i
17 | })) {
18 | return;
19 | }
20 | }
21 |
22 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools
23 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools');
24 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools'))
25 | .development(__DEVELOPMENT__)
26 | .server(rootDir, function() {
27 | require('../src/server');
28 | });
29 |
--------------------------------------------------------------------------------
/webpack/webpack-dev-server.js:
--------------------------------------------------------------------------------
1 | var Express = require('express');
2 | var webpack = require('webpack');
3 |
4 | var config = require('../src/config');
5 | var webpackConfig = require('./dev.config');
6 | var compiler = webpack(webpackConfig);
7 |
8 | var host = process.env.HOST || 'localhost';
9 | var port = parseInt(config.port, 10) + 1 || 3001;
10 | var serverOptions = {
11 | contentBase: 'http://' + host + ':' + port,
12 | quiet: true,
13 | noInfo: true,
14 | hot: true,
15 | inline: true,
16 | lazy: false,
17 | publicPath: webpackConfig.output.publicPath,
18 | headers: {'Access-Control-Allow-Origin': '*'},
19 | stats: {colors: true}
20 | };
21 |
22 | var app = new Express();
23 |
24 | app.use(require('webpack-dev-middleware')(compiler, serverOptions));
25 | app.use(require('webpack-hot-middleware')(compiler));
26 |
27 | app.listen(port, function onAppListening(err) {
28 | if (err) {
29 | console.error(err);
30 | } else {
31 | console.info('==> 🚧 Webpack development server listening on port %s', port);
32 | }
33 | });
34 |
--------------------------------------------------------------------------------
/src/redux/modules/info.js:
--------------------------------------------------------------------------------
1 | const LOAD = 'redux-example/LOAD';
2 | const LOAD_SUCCESS = 'redux-example/LOAD_SUCCESS';
3 | const LOAD_FAIL = 'redux-example/LOAD_FAIL';
4 |
5 | const initialState = {
6 | loaded: false
7 | };
8 |
9 | export default function info(state = initialState, action = {}) {
10 | switch (action.type) {
11 | case LOAD:
12 | return {
13 | ...state,
14 | loading: true
15 | };
16 | case LOAD_SUCCESS:
17 | return {
18 | ...state,
19 | loading: false,
20 | loaded: true,
21 | data: action.result
22 | };
23 | case LOAD_FAIL:
24 | return {
25 | ...state,
26 | loading: false,
27 | loaded: false,
28 | error: action.error
29 | };
30 | default:
31 | return state;
32 | }
33 | }
34 |
35 | export function isLoaded(globalState) {
36 | return globalState.info && globalState.info.loaded;
37 | }
38 |
39 | export function load() {
40 | return {
41 | types: [LOAD, LOAD_SUCCESS, LOAD_FAIL],
42 | promise: (client) => client.get('/loadInfo')
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/InfoBar/InfoBar.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {bindActionCreators} from 'redux';
3 | import {connect} from 'react-redux';
4 | import {load} from 'redux/modules/info';
5 |
6 | @connect(
7 | state => ({info: state.info.data}),
8 | dispatch => bindActionCreators({load}, dispatch))
9 | export default class InfoBar extends Component {
10 | static propTypes = {
11 | info: PropTypes.object,
12 | load: PropTypes.func.isRequired
13 | }
14 |
15 | render() {
16 | const {info, load} = this.props; // eslint-disable-line no-shadow
17 | const styles = require('./InfoBar.scss');
18 | return (
19 |
20 |
21 | This is an info bar
22 | {' '}
23 | {info ? info.message : 'no info!'}
24 | {info && new Date(info.time).toString()}
25 | Reload from server
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/containers/Home/Home.scss:
--------------------------------------------------------------------------------
1 | @import "../../theme/variables.scss";
2 |
3 | .home {
4 | dd {
5 | margin-bottom: 15px;
6 | }
7 | }
8 | .masthead {
9 | background: #2d2d2d;
10 | padding: 40px 20px;
11 | color: white;
12 | text-align: center;
13 | .logo {
14 | $size: 200px;
15 | margin: auto;
16 | height: $size;
17 | width: $size;
18 | border-radius: $size / 2;
19 | border: 1px solid $cyan;
20 | box-shadow: inset 0 0 10px $cyan;
21 | vertical-align: middle;
22 | p {
23 | line-height: $size;
24 | margin: 0px;
25 | }
26 | img {
27 | width: 75%;
28 | margin: auto;
29 | }
30 | }
31 | h1 {
32 | color: $cyan;
33 | font-size: 4em;
34 | }
35 | h2 {
36 | color: #ddd;
37 | font-size: 2em;
38 | margin: 20px;
39 | }
40 | a {
41 | color: #ddd;
42 | }
43 | p {
44 | margin: 10px;
45 | }
46 | .humility {
47 | color: $humility;
48 | a {
49 | color: $humility;
50 | }
51 | }
52 | .github {
53 | font-size: 1.5em;
54 | }
55 | }
56 |
57 | .counterContainer {
58 | text-align: center;
59 | margin: 20px;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/GithubButton/GithubButton.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 |
3 | export default class GithubButton extends Component {
4 | static propTypes = {
5 | user: PropTypes.string.isRequired,
6 | repo: PropTypes.string.isRequired,
7 | type: PropTypes.oneOf(['star', 'watch', 'fork', 'follow']).isRequired,
8 | width: PropTypes.number.isRequired,
9 | height: PropTypes.number.isRequired,
10 | count: PropTypes.bool,
11 | large: PropTypes.bool
12 | }
13 |
14 | render() {
15 | const {user, repo, type, width, height, count, large} = this.props;
16 | let src = `https://ghbtns.com/github-btn.html?user=${user}&repo=${repo}&type=${type}`;
17 | if (count) {
18 | src += '&count=true';
19 | }
20 | if (large) {
21 | src += '&size=large';
22 | }
23 | return (
24 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Erik Rasmussen
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 |
--------------------------------------------------------------------------------
/src/helpers/makeRouteHooksSafe.js:
--------------------------------------------------------------------------------
1 | import { createRoutes } from 'react-router/lib/RouteUtils';
2 |
3 | // Wrap the hooks so they don't fire if they're called before
4 | // the store is initialised. This only happens when doing the first
5 | // client render of a route that has an onEnter hook
6 | function makeHooksSafe(routes, store) {
7 | if (Array.isArray(routes)) {
8 | return routes.map((route) => makeHooksSafe(route, store));
9 | }
10 |
11 | const onEnter = routes.onEnter;
12 |
13 | if (onEnter) {
14 | routes.onEnter = function safeOnEnter(...args) {
15 | try {
16 | store.getState();
17 | } catch (err) {
18 | if (onEnter.length === 3) {
19 | args[2]();
20 | }
21 |
22 | // There's no store yet so ignore the hook
23 | return;
24 | }
25 |
26 | onEnter.apply(null, args);
27 | };
28 | }
29 |
30 | if (routes.childRoutes) {
31 | makeHooksSafe(routes.childRoutes, store);
32 | }
33 |
34 | if (routes.indexRoute) {
35 | makeHooksSafe(routes.indexRoute, store);
36 | }
37 |
38 | return routes;
39 | }
40 |
41 | export default function makeRouteHooksSafe(_getRoutes) {
42 | return (store) => makeHooksSafe(createRoutes(_getRoutes(store)), store);
43 | }
44 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | require('babel-core/polyfill');
2 |
3 | const environment = {
4 | development: {
5 | isProduction: false
6 | },
7 | production: {
8 | isProduction: true
9 | }
10 | }[process.env.NODE_ENV || 'development'];
11 |
12 | module.exports = Object.assign({
13 | port: process.env.PORT,
14 | apiPort: process.env.APIPORT,
15 | app: {
16 | title: 'React Redux Example',
17 | description: 'All the modern best practices in one example.',
18 | meta: {
19 | charSet: 'utf-8',
20 | property: {
21 | 'og:site_name': 'React Redux Example',
22 | 'og:image': 'https://react-redux.herokuapp.com/logo.jpg',
23 | 'og:locale': 'en_US',
24 | 'og:title': 'React Redux Example',
25 | 'og:description': 'All the modern best practices in one example.',
26 | 'twitter:card': 'summary',
27 | 'twitter:site': '@erikras',
28 | 'twitter:creator': '@erikras',
29 | 'twitter:title': 'React Redux Example',
30 | 'twitter:description': 'All the modern best practices in one example.',
31 | 'twitter:image': 'https://react-redux.herokuapp.com/logo.jpg',
32 | 'twitter:image:width': '200',
33 | 'twitter:image:height': '200'
34 | }
35 | }
36 | }
37 | }, environment);
38 |
--------------------------------------------------------------------------------
/src/containers/LoginSuccess/LoginSuccess.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 | import * as authActions from 'redux/modules/auth';
4 |
5 | @connect(
6 | state => ({user: state.auth.user}),
7 | authActions)
8 | export default
9 | class LoginSuccess extends Component {
10 | static propTypes = {
11 | user: PropTypes.object,
12 | logout: PropTypes.func
13 | }
14 |
15 | render() {
16 | const {user, logout} = this.props;
17 | return (user &&
18 |
19 |
Login Success
20 |
21 |
22 |
Hi, {user.name}. You have just successfully logged in, and were forwarded here
23 | by componentWillReceiveProps() in App.js, which is listening to
24 | the auth reducer via redux @connect. How exciting!
25 |
26 |
27 |
28 | The same function will forward you to / should you chose to log out. The choice is yours...
29 |
30 |
31 |
32 | {' '}Log Out
33 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/redux/create.js:
--------------------------------------------------------------------------------
1 | import { createStore as _createStore, applyMiddleware, compose } from 'redux';
2 | import createMiddleware from './middleware/clientMiddleware';
3 | import transitionMiddleware from './middleware/transitionMiddleware';
4 |
5 | export default function createStore(reduxReactRouter, getRoutes, createHistory, client, data) {
6 | const middleware = [createMiddleware(client), transitionMiddleware];
7 |
8 | let finalCreateStore;
9 | if (__DEVELOPMENT__ && __CLIENT__ && __DEVTOOLS__) {
10 | const { persistState } = require('redux-devtools');
11 | const DevTools = require('../containers/DevTools/DevTools');
12 | finalCreateStore = compose(
13 | applyMiddleware(...middleware),
14 | DevTools.instrument(),
15 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
16 | )(_createStore);
17 | } else {
18 | finalCreateStore = applyMiddleware(...middleware)(_createStore);
19 | }
20 |
21 | finalCreateStore = reduxReactRouter({ getRoutes, createHistory })(finalCreateStore);
22 |
23 | const reducer = require('./modules/reducer');
24 | const store = finalCreateStore(reducer, data);
25 |
26 | if (__DEVELOPMENT__ && module.hot) {
27 | module.hot.accept('./modules/reducer', () => {
28 | store.replaceReducer(require('./modules/reducer'));
29 | });
30 | }
31 |
32 | return store;
33 | }
34 |
--------------------------------------------------------------------------------
/src/redux/middleware/transitionMiddleware.js:
--------------------------------------------------------------------------------
1 | import {ROUTER_DID_CHANGE} from 'redux-router/lib/constants';
2 | import getDataDependencies from '../../helpers/getDataDependencies';
3 |
4 | const locationsAreEqual = (locA, locB) => (locA.pathname === locB.pathname) && (locA.search === locB.search);
5 |
6 | export default ({getState, dispatch}) => next => action => {
7 | if (action.type === ROUTER_DID_CHANGE) {
8 | if (getState().router && locationsAreEqual(action.payload.location, getState().router.location)) {
9 | return next(action);
10 | }
11 |
12 | const {components, location, params} = action.payload;
13 | const promise = new Promise((resolve) => {
14 |
15 | const doTransition = () => {
16 | next(action);
17 | Promise.all(getDataDependencies(components, getState, dispatch, location, params, true))
18 | .then(resolve, resolve);
19 | };
20 |
21 | Promise.all(getDataDependencies(components, getState, dispatch, location, params))
22 | .then(doTransition, doTransition);
23 | });
24 |
25 | if (__SERVER__) {
26 | // router state is null until ReduxRouter is created so we can use this to store
27 | // our promise to let the server know when it can render
28 | getState().router = promise;
29 | }
30 |
31 | return promise;
32 | }
33 |
34 | return next(action);
35 | };
36 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | { "extends": "eslint-config-airbnb",
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "mocha": true
6 | },
7 | "rules": {
8 | "react/jsx-uses-react": 2,
9 | "react/jsx-uses-vars": 2,
10 | "react/react-in-jsx-scope": 2,
11 | "react/jsx-quotes": 0,
12 | "import/default": 0,
13 | "import/no-duplicates": 0,
14 | "import/named": 0,
15 | "import/namespace": 0,
16 | "import/no-unresolved": 0,
17 | "import/no-named-as-default": 2,
18 | "jsx-quotes": 2,
19 | // Temporarirly disabled due to a possible bug in babel-eslint (todomvc example)
20 | "block-scoped-var": 0,
21 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved
22 | "padded-blocks": 0,
23 | "comma-dangle": 0, // not sure why airbnb turned this on. gross!
24 | "indent": [2, 2, {"SwitchCase": 1}],
25 | "no-console": 0,
26 | "no-alert": 0
27 | },
28 | "plugins": [
29 | "react", "import"
30 | ],
31 | "settings": {
32 | "import/parser": "babel-eslint",
33 | "import/resolve": {
34 | moduleDirectory: ["node_modules", "src"]
35 | }
36 | },
37 | "globals": {
38 | "__DEVELOPMENT__": true,
39 | "__CLIENT__": true,
40 | "__SERVER__": true,
41 | "__DISABLE_SSR__": true,
42 | "__DEVTOOLS__": true,
43 | "socket": true,
44 | "webpackIsomorphicTools": true
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/helpers/ApiClient.js:
--------------------------------------------------------------------------------
1 | import superagent from 'superagent';
2 | import config from '../config';
3 |
4 | const methods = ['get', 'post', 'put', 'patch', 'del'];
5 |
6 | function formatUrl(path) {
7 | const adjustedPath = path[0] !== '/' ? '/' + path : path;
8 | if (__SERVER__) {
9 | // Prepend host and port of the API server to the path.
10 | return 'http://localhost:' + config.apiPort + adjustedPath;
11 | }
12 | // Prepend `/api` to relative URL, to proxy to API server.
13 | return '/api' + adjustedPath;
14 | }
15 |
16 | /*
17 | * This silly underscore is here to avoid a mysterious "ReferenceError: ApiClient is not defined" error.
18 | * See Issue #14. https://github.com/erikras/react-redux-universal-hot-example/issues/14
19 | *
20 | * Remove it at your own risk.
21 | */
22 | class _ApiClient {
23 | constructor(req) {
24 | methods.forEach((method) =>
25 | this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
26 | const request = superagent[method](formatUrl(path));
27 |
28 | if (params) {
29 | request.query(params);
30 | }
31 |
32 | if (__SERVER__ && req.get('cookie')) {
33 | request.set('cookie', req.get('cookie'));
34 | }
35 |
36 | if (data) {
37 | request.send(data);
38 | }
39 |
40 | request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body));
41 | }));
42 | }
43 | }
44 |
45 | const ApiClient = _ApiClient;
46 |
47 | export default ApiClient;
48 |
--------------------------------------------------------------------------------
/src/helpers/__tests__/getStatusFromRoutes-test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import getStatusFromRoutes from '../getStatusFromRoutes';
3 |
4 | describe('getStatusFromRoutes', () => {
5 |
6 | it('should return null when no routes have status code', () => {
7 | const status = getStatusFromRoutes([
8 | {}, {}
9 | ]);
10 |
11 | expect(status).to.equal(null);
12 | });
13 |
14 | it('should return the only status code', () => {
15 | const status = getStatusFromRoutes([
16 | {status: 404}
17 | ]);
18 |
19 | expect(status).to.equal(404);
20 | });
21 |
22 | it('should return the only status code when other routes have none', () => {
23 | const status = getStatusFromRoutes([
24 | {status: 404}, {}, {}
25 | ]);
26 |
27 | expect(status).to.equal(404);
28 | });
29 |
30 | it('should return the last status code when later routes have none', () => {
31 | const status = getStatusFromRoutes([
32 | {status: 200}, {status: 404}, {}
33 | ]);
34 |
35 | expect(status).to.equal(404);
36 | });
37 |
38 | it('should return the last status code when previous routes have one', () => {
39 | const status = getStatusFromRoutes([
40 | {status: 200}, {}, {status: 404}
41 | ]);
42 |
43 | expect(status).to.equal(404);
44 | });
45 |
46 | it('should return the last status code', () => {
47 | const status = getStatusFromRoutes([
48 | {}, {}, {status: 404}
49 | ]);
50 |
51 | expect(status).to.equal(404);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {IndexRoute, Route} from 'react-router';
3 | import { isLoaded as isAuthLoaded, load as loadAuth } from 'redux/modules/auth';
4 | import {
5 | App,
6 | Chat,
7 | Home,
8 | Widgets,
9 | About,
10 | Login,
11 | LoginSuccess,
12 | Survey,
13 | NotFound,
14 | Uploader
15 | } from 'containers';
16 |
17 | export default (store) => {
18 | const requireLogin = (nextState, replaceState, cb) => {
19 | function checkAuth() {
20 | const { auth: { user }} = store.getState();
21 | if (!user) {
22 | // oops, not logged in, so can't be here!
23 | replaceState(null, '/');
24 | }
25 | cb();
26 | }
27 |
28 | if (!isAuthLoaded(store.getState())) {
29 | store.dispatch(loadAuth()).then(checkAuth);
30 | } else {
31 | checkAuth();
32 | }
33 | };
34 |
35 | /**
36 | * Please keep routes in alphabetical order
37 | */
38 | return (
39 |
40 | { /* Home (main) route */ }
41 |
42 |
43 | { /* Routes requiring login */ }
44 |
45 |
46 |
47 |
48 |
49 | { /* Routes */ }
50 |
51 |
52 |
53 |
54 |
55 |
56 | { /* Catch all route */ }
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/containers/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 | import DocumentMeta from 'react-document-meta';
4 | import * as authActions from 'redux/modules/auth';
5 |
6 | @connect(
7 | state => ({user: state.auth.user}),
8 | authActions)
9 | export default class Login extends Component {
10 | static propTypes = {
11 | user: PropTypes.object,
12 | login: PropTypes.func,
13 | logout: PropTypes.func
14 | }
15 |
16 | handleSubmit(event) {
17 | event.preventDefault();
18 | const input = this.refs.username;
19 | this.props.login(input.value);
20 | input.value = '';
21 | }
22 |
23 | render() {
24 | const {user, logout} = this.props;
25 | const styles = require('./Login.scss');
26 | return (
27 |
28 |
29 |
Login
30 | {!user &&
31 |
32 |
37 |
This will "log you in" as this user, storing the username in the session of the API server.
38 |
39 | }
40 | {user &&
41 |
42 |
You are currently logged in as {user.name}.
43 |
44 |
45 | {' '}Log Out
46 |
47 |
48 | }
49 |
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/containers/App/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import DocumentMeta from 'react-document-meta';
4 | import { isLoaded as isInfoLoaded, load as loadInfo } from 'redux/modules/info';
5 | import { isLoaded as isAuthLoaded, load as loadAuth, logout } from 'redux/modules/auth';
6 | import { pushState } from 'redux-router';
7 | import config from '../../config';
8 |
9 | @connect(
10 | state => ({user: state.auth.user}),
11 | {logout, pushState})
12 | export default class App extends Component {
13 | static propTypes = {
14 | children: PropTypes.object.isRequired,
15 | user: PropTypes.object,
16 | logout: PropTypes.func.isRequired,
17 | pushState: PropTypes.func.isRequired
18 | };
19 |
20 | static contextTypes = {
21 | store: PropTypes.object.isRequired
22 | };
23 |
24 | componentWillReceiveProps(nextProps) {
25 | if (!this.props.user && nextProps.user) {
26 | // login
27 | this.props.pushState(null, '/loginSuccess');
28 | } else if (this.props.user && !nextProps.user) {
29 | // logout
30 | this.props.pushState(null, '/');
31 | }
32 | }
33 |
34 | static fetchData(getState, dispatch) {
35 | const promises = [];
36 | if (!isInfoLoaded(getState())) {
37 | promises.push(dispatch(loadInfo()));
38 | }
39 | if (!isAuthLoaded(getState())) {
40 | promises.push(dispatch(loadAuth()));
41 | }
42 | return Promise.all(promises);
43 | }
44 |
45 | handleLogout(event) {
46 | event.preventDefault();
47 | this.props.logout();
48 | }
49 |
50 | render() {
51 | return (
52 |
53 |
54 |
55 | {this.props.children}
56 |
57 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/__tests__/InfoBar-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {renderIntoDocument} from 'react-addons-test-utils';
4 | import { expect} from 'chai';
5 | import { InfoBar } from 'components';
6 | import { Provider } from 'react-redux';
7 | import {reduxReactRouter} from 'redux-router';
8 | import createHistory from 'history/lib/createMemoryHistory';
9 | import createStore from 'redux/create';
10 | import ApiClient from 'helpers/ApiClient';
11 | const client = new ApiClient();
12 |
13 | describe('InfoBar', () => {
14 | const mockStore = {
15 | info: {
16 | load: () => {},
17 | loaded: true,
18 | loading: false,
19 | data: {
20 | message: 'This came from the api server',
21 | time: Date.now()
22 | }
23 | }
24 | };
25 |
26 | const store = createStore(reduxReactRouter, null, createHistory, client, mockStore);
27 | const renderer = renderIntoDocument(
28 |
29 |
30 |
31 | );
32 | const dom = ReactDOM.findDOMNode(renderer);
33 |
34 | it('should render correctly', () => {
35 | return expect(renderer).to.be.ok;
36 | });
37 |
38 | it('should render with correct value', () => {
39 | const text = dom.getElementsByTagName('strong')[0].textContent;
40 | expect(text).to.equal(mockStore.info.data.message);
41 | });
42 |
43 | it('should render with a reload button', () => {
44 | const text = dom.getElementsByTagName('button')[0].textContent;
45 | expect(text).to.be.a('string');
46 | });
47 |
48 | it('should render the correct className', () => {
49 | const styles = require('components/InfoBar/InfoBar.scss');
50 | expect(styles.infoBar).to.be.a('string');
51 | expect(dom.className).to.include(styles.infoBar);
52 | });
53 |
54 | });
55 |
--------------------------------------------------------------------------------
/src/containers/About/About.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import DocumentMeta from 'react-document-meta';
3 | import { MiniInfoBar } from 'components';
4 |
5 | export default class About extends Component {
6 | state = {
7 | showKitten: false
8 | }
9 |
10 | handleToggleKitten() {
11 | this.setState({showKitten: !this.state.showKitten});
12 | }
13 |
14 | render() {
15 | const {showKitten} = this.state;
16 | const kitten = require('./kitten.jpg');
17 | return (
18 |
19 |
About Us
20 |
21 |
22 |
This project was orginally created by Erik Rasmussen
23 | (@erikras ), but has since seen many contributions
24 | from the open source community. Thank you to all the contributors .
27 |
28 |
29 |
Mini Bar (not that kind)
30 |
31 |
Hey! You found the mini info bar! The following component is display-only. Note that it shows the same
32 | time as the info bar.
33 |
34 |
35 |
36 |
Images
37 |
38 |
39 | Psst! Would you like to see a kitten?
40 |
41 |
44 | {showKitten ? 'No! Take it away!' : 'Yes! Please!'}
45 |
46 |
47 | {showKitten &&
}
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/theme/bootstrap.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bootstrap configuration for bootstrap-sass-loader
3 | *
4 | * Scripts are disabled to not load jQuery.
5 | * If you depend on Bootstrap scripts consider react-bootstrap instead.
6 | * https://github.com/react-bootstrap/react-bootstrap
7 | *
8 | * In order to keep the bundle size low in production
9 | * disable components you don't use.
10 | *
11 | */
12 |
13 | module.exports = {
14 | preBootstrapCustomizations: './src/theme/variables.scss',
15 | mainSass: './src/theme/bootstrap.overrides.scss',
16 | verbose: false,
17 | debug: false,
18 | scripts: {
19 | transition: false,
20 | alert: false,
21 | button: false,
22 | carousel: false,
23 | collapse: false,
24 | dropdown: false,
25 | modal: false,
26 | tooltip: false,
27 | popover: false,
28 | scrollspy: false,
29 | tab: false,
30 | affix: false
31 | },
32 | styles: {
33 | mixins: true,
34 | normalize: true,
35 | print: true,
36 | glyphicons: true,
37 | scaffolding: true,
38 | type: true,
39 | code: true,
40 | grid: true,
41 | tables: true,
42 | forms: true,
43 | buttons: true,
44 | 'component-animations': true,
45 | dropdowns: true,
46 | 'button-groups': true,
47 | 'input-groups': true,
48 | navs: true,
49 | navbar: true,
50 | breadcrumbs: true,
51 | pagination: true,
52 | pager: true,
53 | labels: true,
54 | badges: true,
55 | jumbotron: true,
56 | thumbnails: true,
57 | alerts: true,
58 | 'progress-bars': true,
59 | media: true,
60 | 'list-group': true,
61 | panels: true,
62 | wells: true,
63 | 'responsive-embed': true,
64 | close: true,
65 | modals: true,
66 | tooltip: true,
67 | popovers: true,
68 | carousel: true,
69 | utilities: true,
70 | 'responsive-utilities': true
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/src/utils/validation.js:
--------------------------------------------------------------------------------
1 | const isEmpty = value => value === undefined || value === null || value === '';
2 | const join = (rules) => (value, data) => rules.map(rule => rule(value, data)).filter(error => !!error)[0 /* first error */ ];
3 |
4 | export function email(value) {
5 | // Let's not start a debate on email regex. This is just for an example app!
6 | if (!isEmpty(value) && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) {
7 | return 'Invalid email address';
8 | }
9 | }
10 |
11 | export function required(value) {
12 | if (isEmpty(value)) {
13 | return 'Required';
14 | }
15 | }
16 |
17 | export function minLength(min) {
18 | return value => {
19 | if (!isEmpty(value) && value.length < min) {
20 | return `Must be at least ${min} characters`;
21 | }
22 | };
23 | }
24 |
25 | export function maxLength(max) {
26 | return value => {
27 | if (!isEmpty(value) && value.length > max) {
28 | return `Must be no more than ${max} characters`;
29 | }
30 | };
31 | }
32 |
33 | export function integer(value) {
34 | if (!Number.isInteger(Number(value))) {
35 | return 'Must be an integer';
36 | }
37 | }
38 |
39 | export function oneOf(enumeration) {
40 | return value => {
41 | if (!~enumeration.indexOf(value)) {
42 | return `Must be one of: ${enumeration.join(', ')}`;
43 | }
44 | };
45 | }
46 |
47 | export function match(field) {
48 | return (value, data) => {
49 | if (data) {
50 | if (value !== data[field]) {
51 | return 'Do not match';
52 | }
53 | }
54 | };
55 | }
56 |
57 | export function createValidator(rules) {
58 | return (data = {}) => {
59 | const errors = {};
60 | Object.keys(rules).forEach((key) => {
61 | const rule = join([].concat(rules[key])); // concat enables both functions and arrays of functions
62 | const error = rule(data[key], data);
63 | if (error) {
64 | errors[key] = error;
65 | }
66 | });
67 | return errors;
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | module.exports = function (config) {
4 | config.set({
5 |
6 | browsers: ['PhantomJS'],
7 |
8 | singleRun: !!process.env.CONTINUOUS_INTEGRATION,
9 |
10 | frameworks: [ 'mocha' ],
11 |
12 | files: [
13 | './node_modules/phantomjs-polyfill/bind-polyfill.js',
14 | 'tests.webpack.js'
15 | ],
16 |
17 | preprocessors: {
18 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ]
19 | },
20 |
21 | reporters: [ 'mocha' ],
22 |
23 | plugins: [
24 | require("karma-webpack"),
25 | require("karma-mocha"),
26 | require("karma-mocha-reporter"),
27 | require("karma-phantomjs-launcher"),
28 | require("karma-sourcemap-loader")
29 | ],
30 |
31 | webpack: {
32 | devtool: 'inline-source-map',
33 | module: {
34 | loaders: [
35 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} },
36 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel']},
37 | { test: /\.json$/, loader: 'json-loader' },
38 | { test: /\.less$/, loader: 'style!css!less' },
39 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' }
40 | ]
41 | },
42 | resolve: {
43 | modulesDirectories: [
44 | 'src',
45 | 'node_modules'
46 | ],
47 | extensions: ['', '.json', '.js']
48 | },
49 | plugins: [
50 | new webpack.IgnorePlugin(/\.json$/),
51 | new webpack.NoErrorsPlugin(),
52 | new webpack.DefinePlugin({
53 | __CLIENT__: true,
54 | __SERVER__: false,
55 | __DEVELOPMENT__: true,
56 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE
57 | })
58 | ]
59 | },
60 |
61 | webpackServer: {
62 | noInfo: true
63 | }
64 |
65 | });
66 | };
67 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | /**
2 | * THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER.
3 | */
4 | import 'babel-core/polyfill';
5 | import React from 'react';
6 | import ReactDOM from 'react-dom';
7 | import createHistory from 'history/lib/createBrowserHistory';
8 | import createStore from './redux/create';
9 | import ApiClient from './helpers/ApiClient';
10 | import io from 'socket.io-client';
11 | import {Provider} from 'react-redux';
12 | import {reduxReactRouter, ReduxRouter} from 'redux-router';
13 |
14 | import getRoutes from './routes';
15 | import makeRouteHooksSafe from './helpers/makeRouteHooksSafe';
16 |
17 | const client = new ApiClient();
18 |
19 | const dest = document.getElementById('content');
20 | const store = createStore(reduxReactRouter, makeRouteHooksSafe(getRoutes), createHistory, client, window.__data);
21 |
22 | function initSocket() {
23 | const socket = io('', {path: '/api/ws', transports: ['polling']});
24 | socket.on('news', (data) => {
25 | console.log(data);
26 | socket.emit('my other event', { my: 'data from client' });
27 | });
28 | socket.on('msg', (data) => {
29 | console.log(data);
30 | });
31 |
32 | return socket;
33 | }
34 |
35 | global.socket = initSocket();
36 |
37 | const component = (
38 |
39 | );
40 |
41 | ReactDOM.render(
42 |
43 | {component}
44 | ,
45 | dest
46 | );
47 |
48 | if (process.env.NODE_ENV !== 'production') {
49 | window.React = React; // enable debugger
50 |
51 | if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) {
52 | console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.');
53 | }
54 | }
55 |
56 | if (__DEVTOOLS__) {
57 | const DevTools = require('./containers/DevTools/DevTools');
58 | ReactDOM.render(
59 |
60 |
61 | {component}
62 |
63 |
64 | ,
65 | dest
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/containers/Chat/Chat.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 |
4 | @connect(
5 | state => ({user: state.auth.user})
6 | )
7 | export default
8 | class Chat extends Component {
9 | static propTypes = {
10 | user: PropTypes.object
11 | };
12 |
13 | state = {
14 | message: '',
15 | messages: []
16 | };
17 |
18 | componentDidMount() {
19 | if (socket && !this.onMsgListener) {
20 | this.onMsgListener = socket.on('msg', this.onMessageReceived.bind(this));
21 |
22 | setTimeout(() => {
23 | socket.emit('history', {offset: 0, length: 100});
24 | }, 100);
25 | }
26 | }
27 |
28 | componentWillUnmount() {
29 | if (socket && this.onMsgListener) {
30 | socket.removeListener('on', this.onMsgListener);
31 | this.onMsgListener = null;
32 | }
33 | }
34 |
35 | onMessageReceived(data) {
36 | const messages = this.state.messages;
37 | messages.push(data);
38 | this.setState({messages});
39 | }
40 |
41 | handleSubmit(event) {
42 | event.preventDefault();
43 |
44 | const msg = this.state.message;
45 |
46 | this.setState({message: ''});
47 |
48 | socket.emit('msg', {
49 | from: this.props.user.name,
50 | text: msg
51 | });
52 | }
53 |
54 | render() {
55 | const style = require('./Chat.scss');
56 | const {user} = this.props;
57 |
58 | return (
59 |
60 |
Chat
61 |
62 | {user &&
63 |
64 |
65 | {this.state.messages.map((msg) => {
66 | return {msg.from}: {msg.text} ;
67 | })}
68 |
69 |
78 |
79 | }
80 |
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/helpers/Html.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import ReactDOM from 'react-dom/server';
3 | import serialize from 'serialize-javascript';
4 | import DocumentMeta from 'react-document-meta';
5 |
6 | /**
7 | * Wrapper component containing HTML metadata and boilerplate tags.
8 | * Used in server-side code only to wrap the string output of the
9 | * rendered route component.
10 | *
11 | * The only thing this component doesn't (and can't) include is the
12 | * HTML doctype declaration, which is added to the rendered output
13 | * by the server.js file.
14 | */
15 | export default class Html extends Component {
16 | static propTypes = {
17 | assets: PropTypes.object,
18 | component: PropTypes.node,
19 | store: PropTypes.object
20 | }
21 |
22 | render() {
23 | const {assets, component, store} = this.props;
24 | const content = component ? ReactDOM.renderToString(component) : '';
25 |
26 | return (
27 |
28 |
29 | {DocumentMeta.renderAsReact()}
30 |
31 |
32 |
33 | {/* styles (will be present only in production with webpack extract text plugin) */}
34 | {Object.keys(assets.styles).map((style, key) =>
35 |
37 | )}
38 |
39 | {/* (will be present only in development mode) */}
40 | {/* outputs a tag with all bootstrap styles + App.scss + it could be CurrentPage.scss. */}
41 | {/* can smoothen the initial style flash (flicker) on page load in development mode. */}
42 | {/* ideally one could also include here the style for the current page (Home.scss, About.scss, etc) */}
43 | { Object.keys(assets.styles).length === 0 ? : null }
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/api/__tests__/api-test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import {mapUrl} from '../utils/url';
3 |
4 | describe('mapUrl', () => {
5 |
6 | it('extracts nothing if both params are undefined', () => {
7 | expect(mapUrl(undefined, undefined)).to.deep.equal({
8 | action: null,
9 | params: []
10 | });
11 | });
12 |
13 | it('extracts nothing if the url is empty', () => {
14 | const url = '';
15 | const splittedUrlPath = url.split('?')[0].split('/').slice(1);
16 | const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};
17 |
18 | expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
19 | action: null,
20 | params: []
21 | });
22 | });
23 |
24 | it('extracts nothing if nothing was found', () => {
25 | const url = '/widget/load/?foo=bar';
26 | const splittedUrlPath = url.split('?')[0].split('/').slice(1);
27 | const availableActions = {a: 1, info: {c: 1, load: () => 'baz'}};
28 |
29 | expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
30 | action: null,
31 | params: []
32 | });
33 | });
34 | it('extracts the available actions and the params from an relative url string with GET params', () => {
35 |
36 | const url = '/widget/load/param1/xzy?foo=bar';
37 | const splittedUrlPath = url.split('?')[0].split('/').slice(1);
38 | const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};
39 |
40 | expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
41 | action: availableActions.widget.load,
42 | params: ['param1', 'xzy']
43 | });
44 | });
45 |
46 | it('extracts the available actions from an url string without GET params', () => {
47 | const url = '/widget/load/?foo=bar';
48 | const splittedUrlPath = url.split('?')[0].split('/').slice(1);
49 | const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};
50 |
51 | expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
52 | action: availableActions.widget.load,
53 | params: ['']
54 | });
55 | });
56 |
57 | it('does not find the avaialble action if deeper nesting is required', () => {
58 | const url = '/widget';
59 | const splittedUrlPath = url.split('?')[0].split('/').slice(1);
60 | const availableActions = {a: 1, widget: {c: 1, load: () => 'baz'}};
61 |
62 | expect(mapUrl(availableActions, splittedUrlPath)).to.deep.equal({
63 | action: null,
64 | params: []
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/helpers/__tests__/getDataDependencies-test.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import React from 'react';
3 | import { div } from 'react-dom';
4 | import getDataDependencies from '../getDataDependencies';
5 |
6 | describe('getDataDependencies', () => {
7 | let getState;
8 | let dispatch;
9 | let location;
10 | let params;
11 | let CompWithFetchData;
12 | let CompWithNoData;
13 | let CompWithFetchDataDeferred;
14 | let ConnectedCompWithFetchData;
15 | let ConnectedCompWithFetchDataDeferred;
16 |
17 | beforeEach(() => {
18 | getState = 'getState';
19 | dispatch = 'dispatch';
20 | location = 'location';
21 | params = 'params';
22 |
23 | CompWithNoData = () =>
24 |
;
25 |
26 | CompWithFetchData = () =>
27 |
;
28 |
29 | CompWithFetchData.fetchData = (_getState, _dispatch, _location, _params) => {
30 | return `fetchData ${_getState} ${_dispatch} ${_location} ${_params}`;
31 | };
32 | CompWithFetchDataDeferred = () =>
33 |
;
34 |
35 | CompWithFetchDataDeferred.fetchDataDeferred = (_getState, _dispatch, _location, _params) => {
36 | return `fetchDataDeferred ${_getState} ${_dispatch} ${_location} ${_params}`;
37 | };
38 |
39 | ConnectedCompWithFetchData = () =>
40 |
;
41 |
42 | ConnectedCompWithFetchData.WrappedComponent = CompWithFetchData;
43 |
44 | ConnectedCompWithFetchDataDeferred = () =>
45 |
;
46 |
47 | ConnectedCompWithFetchDataDeferred.WrappedComponent = CompWithFetchDataDeferred;
48 | });
49 |
50 | it('should get fetchDatas', () => {
51 | const deps = getDataDependencies([
52 | CompWithFetchData,
53 | CompWithNoData,
54 | CompWithFetchDataDeferred,
55 | ConnectedCompWithFetchData,
56 | ConnectedCompWithFetchDataDeferred
57 | ], getState, dispatch, location, params);
58 |
59 | expect(deps).to.deep.equal([
60 | 'fetchData getState dispatch location params',
61 | 'fetchData getState dispatch location params'
62 | ]);
63 | });
64 |
65 | it('should get fetchDataDeferreds', () => {
66 | const deps = getDataDependencies([
67 | CompWithFetchData,
68 | CompWithNoData,
69 | CompWithFetchDataDeferred,
70 | ConnectedCompWithFetchDataDeferred
71 | ], getState, dispatch, location, params, true);
72 |
73 | expect(deps).to.deep.equal([
74 | 'fetchDataDeferred getState dispatch location params',
75 | 'fetchDataDeferred getState dispatch location params'
76 | ]);
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/docs/InlineStyles.md:
--------------------------------------------------------------------------------
1 | # Inline Styles
2 |
3 | In the long term, CSS, LESS and SASS are dead. To keep this project on the bleeding edge, we should drop SASS support in favor of inline styles.
4 |
5 | ## Why?
6 |
7 | I think the case is made pretty strongly in these three presentations.
8 |
9 | Christopher Chedeau | Michael Chan | Colin Megill
10 | --- | --- | ---
11 | [](http://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html) | [](https://www.youtube.com/watch?v=ERB1TJBn32c) | [](https://www.youtube.com/watch?v=NoaxsCi13yQ)
12 |
13 | Clearly this is the direction in which web development is moving.
14 |
15 | ## Why not?
16 |
17 | At the moment, all the inline CSS libraries suffer from some or all of these problems:
18 |
19 | * Client side only
20 | * No vendor auto prefixing (requires `User-Agent` checking on server side)
21 | * No server side media queries, resulting in a flicker on load to adjust to client device width
22 |
23 | Ideally, a library would allow for all the benefits of inline calculable styles, but, in production, would allow some generation of a CSS block, with media queries to handle device width conditionals, to be inserted into the page with a `