From 8ef3fdb6a0ad2db5f2496de60ddd5162044f007e Mon Sep 17 00:00:00 2001
From: Berkeley Martinez <Berkeley@robotie.com>
Date: Wed, 27 Jan 2016 11:34:44 -0800
Subject: [PATCH] Initial move to redux

---
 .eslintrc                                         |   2 +-
 client/err-saga.js                                |   9 -
 client/history-saga.js                            |  69 -----
 client/index.js                                   | 120 +++----
 client/sagas/README.md                            |   0
 client/sagas/err-saga.js                          |  19 ++
 client/sagas/index.js                             |   4 +
 client/sagas/title-saga.js                        |  16 +
 common/app/App.jsx                                | 142 +++++----
 common/app/Cat.js                                 |  65 ----
 common/app/app-stream.jsx                         |  17 -
 common/app/components/Footer/README.md            |   1 +
 common/app/components/Nav/Nav.jsx                 |  22 +-
 common/app/components/NotFound/index.jsx          |  18 +-
 common/app/create-app.jsx                         |  79 +++++
 common/app/create-reducer.js                      |  12 +
 common/app/flux/Actions.js                        |  65 ----
 common/app/flux/Store.js                          | 103 ------
 common/app/flux/index.js                          |   2 -
 common/app/index.js                               |   2 +-
 common/app/middlewares.js                         |   0
 common/app/provide-Store.js                       |  11 +
 common/app/redux/actions.js                       |  21 ++
 common/app/redux/fetch-user-saga.js               |  39 +++
 common/app/redux/index.js                         |   6 +
 common/app/redux/oldActions.js                    |  65 ++++
 common/app/redux/reducer.js                       |  38 +++
 common/app/redux/types.js                         |  14 +
 common/app/routes/FAVS/README.md                  |   1 -
 common/app/routes/Hikes/components/Hike.jsx       | 123 ++++----
 common/app/routes/Hikes/components/Hikes.jsx      | 133 ++++----
 common/app/routes/Hikes/components/Lecture.jsx    | 162 +++++-----
 common/app/routes/Hikes/flux/Actions.js           | 361 ----------------------
 common/app/routes/Hikes/flux/index.js             |   1 -
 common/app/routes/Hikes/index.js                  |   5 -
 common/app/routes/Hikes/redux/actions.js          |  54 ++++
 common/app/routes/Hikes/redux/answer-saga.js      | 128 ++++++++
 common/app/routes/Hikes/redux/fetch-hikes-saga.js |  46 +++
 common/app/routes/Hikes/redux/index.js            |   8 +
 common/app/routes/Hikes/redux/oldActions.js       | 361 ++++++++++++++++++++++
 common/app/routes/Hikes/redux/reducer.js          |  88 ++++++
 common/app/routes/Hikes/redux/types.js            |  23 ++
 common/app/routes/Hikes/redux/utils.js            |  74 +++++
 common/app/routes/Jobs/components/NewJob.jsx      |   2 +-
 common/app/sagas.js                               |   6 +
 common/app/temp.js                                |  40 +++
 common/app/utils/Professor-Context.js             |  42 +++
 common/app/utils/professor-x.js                   | 196 ++++++++++++
 common/app/utils/render-to-string.js              |  52 ++++
 common/app/utils/render.js                        |  26 ++
 common/app/utils/shallow-equals.js                |  37 +++
 common/models/User-Identity.js                    |   2 +-
 common/models/promo.js                            |   2 +-
 common/models/user.js                             |   2 +-
 common/utils/ajax-stream.js                       |   2 +-
 common/utils/services-creator.js                  |  48 +++
 gulpfile.js                                       |  11 +-
 package.json                                      |   8 +
 server/boot/a-extendUser.js                       |   2 +-
 server/boot/a-react.js                            |  57 ++--
 server/boot/certificate.js                        |   2 +-
 server/boot/challenge.js                          |   2 +-
 server/boot/commit.js                             |   2 +-
 server/boot/randomAPIs.js                         |   2 +-
 server/boot/story.js                              |   2 +-
 server/boot/user.js                               |   2 +-
 server/services/hikes.js                          |  10 +-
 server/services/user.js                           |   2 +-
 server/utils/commit.js                            |   2 +-
 server/utils/rx.js                                |   2 +-
 70 files changed, 1976 insertions(+), 1116 deletions(-)
 delete mode 100644 client/err-saga.js
 delete mode 100644 client/history-saga.js
 create mode 100644 client/sagas/README.md
 create mode 100644 client/sagas/err-saga.js
 create mode 100644 client/sagas/index.js
 create mode 100644 client/sagas/title-saga.js
 delete mode 100644 common/app/Cat.js
 delete mode 100644 common/app/app-stream.jsx
 create mode 100644 common/app/components/Footer/README.md
 create mode 100644 common/app/create-app.jsx
 create mode 100644 common/app/create-reducer.js
 delete mode 100644 common/app/flux/Actions.js
 delete mode 100644 common/app/flux/Store.js
 delete mode 100644 common/app/flux/index.js
 create mode 100644 common/app/middlewares.js
 create mode 100644 common/app/provide-Store.js
 create mode 100644 common/app/redux/actions.js
 create mode 100644 common/app/redux/fetch-user-saga.js
 create mode 100644 common/app/redux/index.js
 create mode 100644 common/app/redux/oldActions.js
 create mode 100644 common/app/redux/reducer.js
 create mode 100644 common/app/redux/types.js
 delete mode 100644 common/app/routes/FAVS/README.md
 delete mode 100644 common/app/routes/Hikes/flux/Actions.js
 delete mode 100644 common/app/routes/Hikes/flux/index.js
 create mode 100644 common/app/routes/Hikes/redux/actions.js
 create mode 100644 common/app/routes/Hikes/redux/answer-saga.js
 create mode 100644 common/app/routes/Hikes/redux/fetch-hikes-saga.js
 create mode 100644 common/app/routes/Hikes/redux/index.js
 create mode 100644 common/app/routes/Hikes/redux/oldActions.js
 create mode 100644 common/app/routes/Hikes/redux/reducer.js
 create mode 100644 common/app/routes/Hikes/redux/types.js
 create mode 100644 common/app/routes/Hikes/redux/utils.js
 create mode 100644 common/app/sagas.js
 create mode 100644 common/app/temp.js
 create mode 100644 common/app/utils/Professor-Context.js
 create mode 100644 common/app/utils/professor-x.js
 create mode 100644 common/app/utils/render-to-string.js
 create mode 100644 common/app/utils/render.js
 create mode 100644 common/app/utils/shallow-equals.js
 create mode 100644 common/utils/services-creator.js

diff --git a/.eslintrc b/.eslintrc
index 0e95bec..073f601 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -232,7 +232,7 @@
     "react/jsx-uses-vars": 1,
     "react/no-did-mount-set-state": 2,
     "react/no-did-update-set-state": 2,
-    "react/no-multi-comp": 2,
+    "react/no-multi-comp": [2, { "ignoreStateless": true } ],
     "react/prop-types": 2,
     "react/react-in-jsx-scope": 1,
     "react/self-closing-comp": 1,
diff --git a/client/err-saga.js b/client/err-saga.js
deleted file mode 100644
index d681e4c..0000000
--- a/client/err-saga.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function toastSaga(err$, toast) {
-  err$
-    .doOnNext(() => toast({
-      type: 'error',
-      title: 'Oops, something went wrong',
-      message: `Something went wrong, please try again later`
-    }))
-    .subscribe(err => console.error(err));
-}
diff --git a/client/history-saga.js b/client/history-saga.js
deleted file mode 100644
index f73576d..0000000
--- a/client/history-saga.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Disposable, Observable } from 'rx';
-
-export function location$(history) {
-  return Observable.create(function(observer) {
-    const dispose = history.listen(function(location) {
-      observer.onNext(location);
-    });
-
-    return Disposable.create(() => {
-      dispose();
-    });
-  });
-}
-
-const emptyLocation = {
-  pathname: '',
-  search: '',
-  hash: ''
-};
-
-let prevKey;
-let isSyncing = false;
-export default function historySaga(
-  history,
-  updateLocation,
-  goTo,
-  goBack,
-  routerState$
-) {
-  routerState$.subscribe(
-    location => {
-
-      if (!location) {
-        return null;
-      }
-
-      // store location has changed, update history
-      if (!location.key || location.key !== prevKey) {
-        isSyncing = true;
-        history.transitionTo({ ...emptyLocation, ...location });
-        isSyncing = false;
-      }
-    }
-  );
-
-  location$(history)
-    .doOnNext(location => {
-      prevKey = location.key;
-
-      if (isSyncing) {
-        return null;
-      }
-
-      return updateLocation(location);
-    })
-    .subscribe(() => {});
-
-  goTo
-    .doOnNext((route = '/') => {
-      history.push(route);
-    })
-    .subscribe(() => {});
-
-  goBack
-    .doOnNext(() => {
-      history.goBack();
-    })
-    .subscribe(() => {});
-}
diff --git a/client/index.js b/client/index.js
index 7f14635..92d4d9c 100644
--- a/client/index.js
+++ b/client/index.js
@@ -1,99 +1,71 @@
-import unused from './es6-shims'; // eslint-disable-line
+import './es6-shims';
 import Rx from 'rx';
 import React from 'react';
-import Fetchr from 'fetchr';
-import debugFactory from 'debug';
+import debug from 'debug';
 import { Router } from 'react-router';
+import { routeReducer as routing, syncHistory } from 'react-router-redux';
 import { createLocation, createHistory } from 'history';
-import { hydrate } from 'thundercats';
-import { render$ } from 'thundercats-react';
 
 import app$ from '../common/app';
-import historySaga from './history-saga';
-import errSaga from './err-saga';
+import provideStore from '../common/app/provide-store';
 
-const debug = debugFactory('fcc:client');
+// client specific sagas
+import sagas from './sagas';
+
+// render to observable
+import render from '../common/app/utils/render';
+
+const log = debug('fcc:client');
 const DOMContianer = document.getElementById('fcc');
-const catState = window.__fcc__.data || {};
-const services = new Fetchr({
-  xhrPath: '/services'
-});
+const initialState = window.__fcc__.data;
+
+const serviceOptions = { xhrPath: '/services' };
 
 Rx.config.longStackSupport = !!debug.enabled;
 const history = createHistory();
 const appLocation = createLocation(
   location.pathname + location.search
 );
+const routingMiddleware = syncHistory(history);
 
-// returns an observable
-app$({ history, location: appLocation })
-  .flatMap(
-    ({ AppCat }) => {
-      // instantiate the cat with service
-      const appCat = AppCat(null, services, history);
-      // hydrate the stores
-      return hydrate(appCat, catState).map(() => appCat);
-    },
-    // not using nextLocation at the moment but will be used for
-    // redirects in the future
-    ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
-  )
-  .doOnNext(({ appCat }) => {
-    const appStore$ = appCat.getStore('appStore');
-
-    const {
-      toast,
-      updateLocation,
-      goTo,
-      goBack
-    } = appCat.getActions('appActions');
-
-
-    const routerState$ = appStore$
-      .map(({ location }) => location)
-      .filter(location => !!location);
+const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
+const shouldRouterListenForReplays = !!window.devToolsExtension;
 
-    // set page title
-    appStore$
-      .pluck('title')
-      .distinctUntilChanged()
-      .doOnNext(title => document.title = title)
-      .subscribe(() => {});
+const clientSagaOptions = { doc: document };
 
-    historySaga(
-      history,
-      updateLocation,
-      goTo,
-      goBack,
-      routerState$
-    );
-
-    const err$ = appStore$
-      .pluck('err')
-      .filter(err => !!err)
-      .distinctUntilChanged();
+// returns an observable
+app$({
+  location: appLocation,
+  history,
+  serviceOptions,
+  initialState,
+  middlewares: [
+    routingMiddleware,
+    ...sagas.map(saga => saga(clientSagaOptions))
+  ],
+  reducers: { routing },
+  enhancers: [ devTools ]
+})
+  .flatMap(({ props, store }) => {
 
-    errSaga(err$, toast);
-  })
-  // allow store subscribe to subscribe to actions
-  .delay(10)
-  .flatMap(({ props, appCat }) => {
+    // because of weirdness in react-routers match function
+    // we replace the wrapped returned in props with the first one
+    // we passed in. This might be fixed in react-router 2.0
     props.history = history;
 
-    return render$(
-      appCat,
-      React.createElement(Router, props),
+    if (shouldRouterListenForReplays && store) {
+      log('routing middleware listening for replays');
+      routingMiddleware.listenForReplays(store);
+    }
+
+    log('rendering');
+    return render(
+      provideStore(React.createElement(Router, props), store),
       DOMContianer
     );
   })
   .subscribe(
-    () => {
-      debug('react rendered');
-    },
-    err => {
-      throw err;
-    },
-    () => {
-      debug('react closed subscription');
-    }
+    () => debug('react rendered'),
+    err => { throw err; },
+    () => debug('react closed subscription')
   );
diff --git a/client/sagas/README.md b/client/sagas/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/client/sagas/err-saga.js b/client/sagas/err-saga.js
new file mode 100644
index 0000000..72a80d4
--- /dev/null
+++ b/client/sagas/err-saga.js
@@ -0,0 +1,19 @@
+//   () =>
+//     (store: Store) =>
+//     (next: (action: Action) => Object) =>
+//     errSaga(action: Action) => Object|Void
+export default () => ({ dispatch }) => next => {
+  return function errorSaga(action) {
+    if (!action.error) { return next(action); }
+
+    console.error(action.error);
+    dispatch({
+      type: 'app.makeToast',
+      payload: {
+        type: 'error',
+        title: 'Oops, something went wrong',
+        message: `Something went wrong, please try again later`
+      }
+    });
+  };
+};
diff --git a/client/sagas/index.js b/client/sagas/index.js
new file mode 100644
index 0000000..fc72193
--- /dev/null
+++ b/client/sagas/index.js
@@ -0,0 +1,4 @@
+import errSaga from './err-saga';
+import titleSaga from './title-saga';
+
+export default [errSaga, titleSaga];
diff --git a/client/sagas/title-saga.js b/client/sagas/title-saga.js
new file mode 100644
index 0000000..59f357f
--- /dev/null
+++ b/client/sagas/title-saga.js
@@ -0,0 +1,16 @@
+//   (doc: Object) =>
+//     () =>
+//     (next: (action: Action) => Object) =>
+//     titleSage(action: Action) => Object|Void
+export default (doc) => () => next => {
+  return function titleSage(action) {
+    // get next state
+    const result = next(action);
+    if (action !== 'updateTitle') {
+      return result;
+    }
+    const newTitle = result.app.title;
+    doc.title = newTitle;
+    return result;
+  };
+};
diff --git a/common/app/App.jsx b/common/app/App.jsx
index e3c3fa1..0afb196 100644
--- a/common/app/App.jsx
+++ b/common/app/App.jsx
@@ -1,81 +1,89 @@
 import React, { PropTypes } from 'react';
 import { Row } from 'react-bootstrap';
 import { ToastMessage, ToastContainer } from 'react-toastr';
-import { contain } from 'thundercats-react';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchUser } from './redux/actions';
+import contain from './utils/professor-x';
 
 import Nav from './components/Nav';
 
 const toastMessageFactory = React.createFactory(ToastMessage.animation);
 
-export default contain(
-  {
-    actions: ['appActions'],
-    store: 'appStore',
-    fetchAction: 'appActions.getUser',
-    isPrimed({ username }) {
-      return !!username;
-    },
-    map({
-      username,
-      points,
-      picture,
-      toast
-    }) {
-      return {
-        username,
-        points,
-        picture,
-        toast
-      };
-    },
-    getPayload(props) {
-      return {
-        isPrimed: !!props.username
-      };
-    }
-  },
-  React.createClass({
-    displayName: 'FreeCodeCamp',
+const mapStateToProps = createSelector(
+  state => state.app,
+  ({
+    username,
+    points,
+    picture,
+    toast
+  }) => ({
+    username,
+    points,
+    picture,
+    toast
+  })
+);
+
+const fetchContainerOptions = {
+  fetchAction: 'fetchUser',
+  isPrimed({ username }) {
+    return !!username;
+  }
+};
 
-    propTypes: {
-      appActions: PropTypes.object,
-      children: PropTypes.node,
-      username: PropTypes.string,
-      points: PropTypes.number,
-      picture: PropTypes.string,
-      toast: PropTypes.object
-    },
+// export plain class for testing
+export class FreeCodeCamp extends React.Component {
+  static displayName = 'FreeCodeCamp';
 
-    componentWillReceiveProps({ toast: nextToast = {} }) {
-      const { toast = {} } = this.props;
-      if (toast.id !== nextToast.id) {
-        this.refs.toaster[nextToast.type || 'success'](
-          nextToast.message,
-          nextToast.title,
-          {
-            closeButton: true,
-            timeOut: 10000
-          }
-        );
-      }
-    },
+  static propTypes = {
+    children: PropTypes.node,
+    username: PropTypes.string,
+    points: PropTypes.number,
+    picture: PropTypes.string,
+    toast: PropTypes.object
+  };
 
-    render() {
-      const { username, points, picture } = this.props;
-      const navProps = { username, points, picture };
-      return (
-        <div>
-          <Nav
-            { ...navProps }/>
-          <Row>
-            { this.props.children }
-          </Row>
-          <ToastContainer
-            className='toast-bottom-right'
-            ref='toaster'
-            toastMessageFactory={ toastMessageFactory } />
-        </div>
+  componentWillReceiveProps({ toast: nextToast = {} }) {
+    const { toast = {} } = this.props;
+    if (toast.id !== nextToast.id) {
+      this.refs.toaster[nextToast.type || 'success'](
+        nextToast.message,
+        nextToast.title,
+        {
+          closeButton: true,
+          timeOut: 10000
+        }
       );
     }
-  })
+  }
+
+  render() {
+    const { username, points, picture } = this.props;
+    const navProps = { username, points, picture };
+
+    return (
+      <div>
+        <Nav { ...navProps }/>
+        <Row>
+          { this.props.children }
+        </Row>
+        <ToastContainer
+          className='toast-bottom-right'
+          ref='toaster'
+          toastMessageFactory={ toastMessageFactory } />
+      </div>
+    );
+  }
+}
+
+const wrapComponent = compose(
+  // connect Component to Redux Store
+  connect(mapStateToProps, { fetchUser }),
+  // handles prefetching data
+  contain(fetchContainerOptions)
 );
+
+export default wrapComponent(FreeCodeCamp);
diff --git a/common/app/Cat.js b/common/app/Cat.js
deleted file mode 100644
index 8673cb2..0000000
--- a/common/app/Cat.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Cat } from 'thundercats';
-import stamp from 'stampit';
-import { Disposable, Observable } from 'rx';
-
-import { post$, postJSON$ } from '../utils/ajax-stream.js';
-import { AppActions, AppStore } from './flux';
-import HikesActions from './routes/Hikes/flux';
-import JobActions from './routes/Jobs/flux';
-
-const ajaxStamp = stamp({
-  methods: {
-    postJSON$,
-    post$
-  }
-});
-
-export default Cat().init(({ instance: cat, args: [services] }) => {
-  const serviceStamp = stamp({
-    methods: {
-      readService$(resource, params, config) {
-
-        return Observable.create(function(observer) {
-          services.read(resource, params, config, (err, res) => {
-            if (err) {
-              return observer.onError(err);
-            }
-
-            observer.onNext(res);
-            observer.onCompleted();
-          });
-
-          return Disposable.create(function() {
-            observer.dispose();
-          });
-        });
-      },
-      createService$(resource, params, body, config) {
-        return Observable.create(function(observer) {
-          services.create(resource, params, body, config, (err, res) => {
-            if (err) {
-              return observer.onError(err);
-            }
-
-            observer.onNext(res);
-            observer.onCompleted();
-          });
-
-          return Disposable.create(function() {
-            observer.dispose();
-          });
-        });
-      }
-    }
-  });
-
-  cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services);
-  cat.register(AppActions.compose(serviceStamp), null, services);
-  cat.register(
-    JobActions.compose(serviceStamp, ajaxStamp),
-    null,
-    cat,
-    services
-  );
-  cat.register(AppStore, null, cat);
-});
diff --git a/common/app/app-stream.jsx b/common/app/app-stream.jsx
deleted file mode 100644
index 82d5568..0000000
--- a/common/app/app-stream.jsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import Rx from 'rx';
-import { match } from 'react-router';
-import App from './App.jsx';
-import AppCat from './Cat';
-
-import childRoutes from './routes';
-
-const route$ = Rx.Observable.fromNodeCallback(match);
-
-const routes = Object.assign({ components: App }, childRoutes);
-
-export default function app$({ location, history }) {
-  return route$({ routes, location, history })
-    .map(([nextLocation, props]) => {
-      return { nextLocation, props, AppCat };
-    });
-}
diff --git a/common/app/components/Footer/README.md b/common/app/components/Footer/README.md
new file mode 100644
index 0000000..a8f04d0
--- /dev/null
+++ b/common/app/components/Footer/README.md
@@ -0,0 +1 @@
+Currently not used
diff --git a/common/app/components/Nav/Nav.jsx b/common/app/components/Nav/Nav.jsx
index 205cb26..922c050 100644
--- a/common/app/components/Nav/Nav.jsx
+++ b/common/app/components/Nav/Nav.jsx
@@ -23,20 +23,20 @@ const logoElement = (
 );
 
 const toggleButtonChild = (
-    <Col xs={ 12 }>
-      <span className='hamburger-text'>Menu</span>
-    </Col>
+  <Col xs={ 12 }>
+    <span className='hamburger-text'>Menu</span>
+  </Col>
 );
 
-export default React.createClass({
-  displayName: 'Nav',
+export default class extends React.Component {
+  static displayName = 'Nav';
 
-  propTypes: {
+  static propTypes = {
     points: PropTypes.number,
     picture: PropTypes.string,
     signedIn: PropTypes.bool,
     username: PropTypes.string
-  },
+  };
 
   renderLinks() {
     return navLinks.map(({ content, link, react, target }, index) => {
@@ -63,7 +63,7 @@ export default React.createClass({
         </NavItem>
       );
     });
-  },
+  }
 
   renderPoints(username, points) {
     if (!username) {
@@ -76,7 +76,7 @@ export default React.createClass({
         [ { points } ]
       </FCCNavItem>
     );
-  },
+  }
 
   renderSignin(username, picture) {
     if (username) {
@@ -100,7 +100,7 @@ export default React.createClass({
         </NavItem>
       );
     }
-  },
+  }
 
   render() {
     const { username, points, picture } = this.props;
@@ -124,4 +124,4 @@ export default React.createClass({
       </Navbar>
     );
   }
-});
+}
diff --git a/common/app/components/NotFound/index.jsx b/common/app/components/NotFound/index.jsx
index eb634a2..0032277 100644
--- a/common/app/components/NotFound/index.jsx
+++ b/common/app/components/NotFound/index.jsx
@@ -6,17 +6,21 @@ function goToServer(path) {
   win.location = '/' + path;
 }
 
-export default React.createClass({
-  displayName: 'NotFound',
-  propTypes: {
+export default class extends React.Component {
+  static displayName = 'NotFound';
+
+  static propTypes = {
     params: PropTypes.object
-  },
+  };
+
   componentWillMount() {
     goToServer(this.props.params.splat);
-  },
+  }
+
   componentDidMount() {
-  },
+  }
+
   render() {
     return <span></span>;
   }
-});
+}
diff --git a/common/app/create-app.jsx b/common/app/create-app.jsx
new file mode 100644
index 0000000..27a6cd2
--- /dev/null
+++ b/common/app/create-app.jsx
@@ -0,0 +1,79 @@
+import { Observable } from 'rx';
+import { match } from 'react-router';
+import { compose, createStore, applyMiddleware } from 'redux';
+
+// main app
+import App from './App.jsx';
+// app routes
+import childRoutes from './routes';
+
+// redux
+import createReducer from './create-reducer';
+import middlewares from './middlewares';
+import sagas from './sagas';
+
+// general utils
+import servicesCreator from '../utils/services-creator';
+
+const createRouteProps = Observable.fromNodeCallback(match);
+
+const routes = { components: App, ...childRoutes };
+
+//
+// createApp(settings: {
+//   location?: Location,
+//   history?: History,
+//   initialState?: Object|Void,
+//   serviceOptions?: Object,
+//   middlewares?: Function[],
+//   sideReducers?: Object
+//   enhancers?: Function[],
+//   sagas?: Function[],
+// }) => Observable
+//
+// Either location or history must be defined
+export default function createApp({
+  location,
+  history,
+  initialState,
+  serviceOptions = {},
+  middlewares: sideMiddlewares = [],
+  enhancers: sideEnhancers = [],
+  reducers: sideReducers = {},
+  sagas: sideSagas = []
+}) {
+  const sagaOptions = {
+    services: servicesCreator(null, serviceOptions)
+  };
+
+  const enhancers = [
+    applyMiddleware(
+      ...middlewares,
+      ...sideMiddlewares,
+      ...[ ...sagas, ...sideSagas].map(saga => saga(sagaOptions)),
+    ),
+    // enhancers must come after middlewares
+    // on client side these are things like Redux DevTools
+    ...sideEnhancers
+  ];
+  const reducer = createReducer(sideReducers);
+
+  // create composed store enhancer
+  // use store enhancer function to enhance `createStore` function
+  // call enhanced createStore function with reducer and initialState
+  // to create store
+  const store = compose(...enhancers)(createStore)(reducer, initialState);
+
+  // createRouteProps({
+  //   location: LocationDescriptor,
+  //   history: History,
+  //   routes: Object
+  // }) => Observable
+  return createRouteProps({ routes, location, history })
+    .map(([ nextLocation, props ]) => ({
+      nextLocation,
+      props,
+      reducer,
+      store
+    }));
+}
diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js
new file mode 100644
index 0000000..ee74780
--- /dev/null
+++ b/common/app/create-reducer.js
@@ -0,0 +1,12 @@
+import { combineReducers } from 'redux';
+
+import { reducer as app } from './redux';
+import { reducer as hikesApp } from './routes/Hikes/redux';
+
+export default function createReducer(sideReducers = {}) {
+  return combineReducers({
+    ...sideReducers,
+    app,
+    hikesApp
+  });
+}
diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js
deleted file mode 100644
index 8fb5025..0000000
--- a/common/app/flux/Actions.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Actions } from 'thundercats';
-import { Observable } from 'rx';
-
-
-export default Actions({
-  shouldBindMethods: true,
-  refs: { displayName: 'AppActions' },
-
-  setTitle(title = 'Learn To Code') {
-    return { title: title + ' | Free Code Camp' };
-  },
-
-  getUser() {
-    return this.readService$('user', null, null)
-      .map(({
-        username,
-        picture,
-        progressTimestamps = [],
-        isFrontEndCert,
-        isBackEndCert,
-        isFullStackCert
-      }) => {
-        return {
-          username,
-          picture,
-          points: progressTimestamps.length,
-          isFrontEndCert,
-          isBackEndCert,
-          isFullStackCert
-        };
-      })
-      .catch(err => Observable.just({ err }));
-  },
-
-  // routing
-  // goTo(path: String) => path
-  goTo: null,
-
-  // goBack(arg?) => arg?
-  goBack: null,
-
-  // toast(args: { type?: String, message: String, title: String }) => args
-  toast(args) {
-    return {
-      transform(state) {
-        return {
-          ...state,
-          toast: {
-            ...args,
-            id: state.toast && state.toast.id ? state.toast.id : 1
-          }
-        };
-      }
-    };
-  },
-
-  // updateLocation(location: { pathname: String }) => location
-  updateLocation(location) {
-    return {
-      transform(state) {
-        return { ...state, location };
-      }
-    };
-  }
-});
diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js
deleted file mode 100644
index 49f7cb3..0000000
--- a/common/app/flux/Store.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Store } from 'thundercats';
-
-const { createRegistrar, setter, fromMany } = Store;
-const initValue = {
-  title: 'Learn To Code | Free Code Camp',
-  username: null,
-  picture: null,
-  points: 0,
-  hikesApp: {
-    hikes: [],
-    // lecture state
-    currentHike: {},
-    showQuestions: false
-  },
-  jobsApp: {
-    showModal: false
-  }
-};
-
-export default Store({
-  refs: {
-    displayName: 'AppStore',
-    value: initValue
-  },
-  init({ instance: store, args: [cat] }) {
-    const register = createRegistrar(store);
-    // app
-    const {
-      updateLocation,
-      getUser,
-      setTitle,
-      toast
-    } = cat.getActions('appActions');
-
-    register(
-      fromMany(
-        setter(
-          fromMany(
-            getUser,
-            setTitle
-          )
-        ),
-        updateLocation,
-        toast
-      )
-    );
-
-    // hikes
-    const {
-      toggleQuestions,
-      fetchHikes,
-      resetHike,
-      grabQuestion,
-      releaseQuestion,
-      moveQuestion,
-      answer
-    } = cat.getActions('hikesActions');
-
-    register(
-      fromMany(
-        toggleQuestions,
-        fetchHikes,
-        resetHike,
-        grabQuestion,
-        releaseQuestion,
-        moveQuestion,
-        answer
-      )
-    );
-
-
-    // jobs
-    const {
-      findJob,
-      saveJobToDb,
-      getJob,
-      getJobs,
-      openModal,
-      closeModal,
-      handleForm,
-      getSavedForm,
-      setPromoCode,
-      applyCode,
-      clearPromo
-    } = cat.getActions('JobActions');
-
-    register(
-      fromMany(
-        findJob,
-        saveJobToDb,
-        getJob,
-        getJobs,
-        openModal,
-        closeModal,
-        handleForm,
-        getSavedForm,
-        setPromoCode,
-        applyCode,
-        clearPromo
-      )
-    );
-  }
-});
diff --git a/common/app/flux/index.js b/common/app/flux/index.js
deleted file mode 100644
index a07e138..0000000
--- a/common/app/flux/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export AppActions from './Actions';
-export AppStore from './Store';
diff --git a/common/app/index.js b/common/app/index.js
index 99924ac..1a3fead 100644
--- a/common/app/index.js
+++ b/common/app/index.js
@@ -1 +1 @@
-export default from './app-stream.jsx';
+export default from './create-app.jsx';
diff --git a/common/app/middlewares.js b/common/app/middlewares.js
new file mode 100644
index 0000000..e69de29
diff --git a/common/app/provide-Store.js b/common/app/provide-Store.js
new file mode 100644
index 0000000..a569f64
--- /dev/null
+++ b/common/app/provide-Store.js
@@ -0,0 +1,11 @@
+/* eslint-disable react/display-name */
+import React from 'react';
+import { Provider } from 'react-redux';
+
+export default function provideStore(element, store) {
+  return React.createElement(
+    Provider,
+    { store },
+    element
+  );
+}
diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js
new file mode 100644
index 0000000..cdc221a
--- /dev/null
+++ b/common/app/redux/actions.js
@@ -0,0 +1,21 @@
+import { createAction } from 'redux-actions';
+import types from './types';
+
+// updateTitle(title: String) => Action
+export const updateTitle = createAction(types.updateTitle);
+
+// makeToast({ type?: String, message: String, title: String }) => Action
+export const makeToast = createAction(
+  types.makeToast,
+  toast => toast.type ? toast : (toast.type = 'info', toast)
+);
+
+// fetchUser() => Action
+// used in combination with fetch-user-saga
+export const fetchUser = createAction(types.fetchUser);
+
+// setUser(userInfo: Object) => Action
+export const setUser = createAction(types.setUser);
+
+// updatePoints(points: Number) => Action
+export const updatePoints = createAction(types.updatePoints);
diff --git a/common/app/redux/fetch-user-saga.js b/common/app/redux/fetch-user-saga.js
new file mode 100644
index 0000000..4f01aea
--- /dev/null
+++ b/common/app/redux/fetch-user-saga.js
@@ -0,0 +1,39 @@
+import { Observable } from 'rx';
+import { handleError, setUser, fetchUser } from './types';
+
+export default ({ services }) => ({ dispatch }) => next => {
+  return function getUserSaga(action) {
+    if (action.type !== fetchUser) {
+      return next(action);
+    }
+
+    return services.readService$({ service: 'user' })
+      .map(({
+        username,
+        picture,
+        progressTimestamps = [],
+        isFrontEndCert,
+        isBackEndCert,
+        isFullStackCert
+      }) => {
+        return {
+          type: setUser,
+          payload: {
+            username,
+            picture,
+            points: progressTimestamps.length,
+            isFrontEndCert,
+            isBackEndCert,
+            isFullStackCert,
+            isSignedIn: true
+          }
+        };
+      })
+      .catch(error => Observable.just({
+        type: handleError,
+        error
+      }))
+      .doOnNext(dispatch);
+  };
+};
+
diff --git a/common/app/redux/index.js b/common/app/redux/index.js
new file mode 100644
index 0000000..d0e8381
--- /dev/null
+++ b/common/app/redux/index.js
@@ -0,0 +1,6 @@
+export { default as reducer } from './reducer';
+export { default as actions } from './actions';
+export { default as types } from './types';
+
+import fetchUserSaga from './fetch-user-saga';
+export const sagas = [ fetchUserSaga ];
diff --git a/common/app/redux/oldActions.js b/common/app/redux/oldActions.js
new file mode 100644
index 0000000..8fb5025
--- /dev/null
+++ b/common/app/redux/oldActions.js
@@ -0,0 +1,65 @@
+import { Actions } from 'thundercats';
+import { Observable } from 'rx';
+
+
+export default Actions({
+  shouldBindMethods: true,
+  refs: { displayName: 'AppActions' },
+
+  setTitle(title = 'Learn To Code') {
+    return { title: title + ' | Free Code Camp' };
+  },
+
+  getUser() {
+    return this.readService$('user', null, null)
+      .map(({
+        username,
+        picture,
+        progressTimestamps = [],
+        isFrontEndCert,
+        isBackEndCert,
+        isFullStackCert
+      }) => {
+        return {
+          username,
+          picture,
+          points: progressTimestamps.length,
+          isFrontEndCert,
+          isBackEndCert,
+          isFullStackCert
+        };
+      })
+      .catch(err => Observable.just({ err }));
+  },
+
+  // routing
+  // goTo(path: String) => path
+  goTo: null,
+
+  // goBack(arg?) => arg?
+  goBack: null,
+
+  // toast(args: { type?: String, message: String, title: String }) => args
+  toast(args) {
+    return {
+      transform(state) {
+        return {
+          ...state,
+          toast: {
+            ...args,
+            id: state.toast && state.toast.id ? state.toast.id : 1
+          }
+        };
+      }
+    };
+  },
+
+  // updateLocation(location: { pathname: String }) => location
+  updateLocation(location) {
+    return {
+      transform(state) {
+        return { ...state, location };
+      }
+    };
+  }
+});
diff --git a/common/app/redux/reducer.js b/common/app/redux/reducer.js
new file mode 100644
index 0000000..1aaece3
--- /dev/null
+++ b/common/app/redux/reducer.js
@@ -0,0 +1,38 @@
+import { handleActions } from 'redux-actions';
+import types from './types';
+
+export default handleActions(
+  {
+    [types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
+      ...state,
+      title: payload + ' | Free Code Camp'
+    }),
+
+    [types.makeToast]: (state, { payload: toast }) => ({
+      ...state,
+      toast: {
+        ...toast,
+        id: state.toast && state.toast.id ? state.toast.id : 1
+      }
+    }),
+
+    [types.setUser]: (state, { payload: user }) => ({ ...state, ...user }),
+
+    [types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
+      ...state,
+      points
+    }),
+
+    [types.updatePoints]: (state, { payload: points }) => ({
+      ...state,
+      points
+    })
+  },
+  {
+    title: 'Learn To Code | Free Code Camp',
+    username: null,
+    picture: null,
+    points: 0,
+    isSignedIn: false
+  }
+);
diff --git a/common/app/redux/types.js b/common/app/redux/types.js
new file mode 100644
index 0000000..1612245
--- /dev/null
+++ b/common/app/redux/types.js
@@ -0,0 +1,14 @@
+const types = [
+  'updateTitle',
+
+  'fetchUser',
+  'setUser',
+
+  'makeToast',
+  'updatePoints',
+  'handleError'
+];
+
+export default types
+  // make into object with signature { type: nameSpace[type] };
+  .reduce((types, type) => ({ ...types, [type]: `app.${type}` }), {});
diff --git a/common/app/routes/FAVS/README.md b/common/app/routes/FAVS/README.md
deleted file mode 100644
index c02922d..0000000
--- a/common/app/routes/FAVS/README.md
+++ /dev/null
@@ -1 +0,0 @@
-future home of FAVS app
diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/Hikes/components/Hike.jsx
index 85ebdb4..6f75ae7 100644
--- a/common/app/routes/Hikes/components/Hike.jsx
+++ b/common/app/routes/Hikes/components/Hike.jsx
@@ -1,63 +1,74 @@
 import React, { PropTypes } from 'react';
-import { contain } from 'thundercats-react';
+import { connect } from 'react-redux';
 import { Col, Row } from 'react-bootstrap';
+import { createSelector } from 'reselect';
 
 import Lecture from './Lecture.jsx';
 import Questions from './Questions.jsx';
+import { resetHike } from '../redux/actions';
 
-export default contain(
-  {
-    actions: ['hikesActions']
-  },
-  React.createClass({
-    displayName: 'Hike',
-
-    propTypes: {
-      currentHike: PropTypes.object,
-      hikesActions: PropTypes.object,
-      params: PropTypes.object,
-      showQuestions: PropTypes.bool
-    },
-
-    componentWillUnmount() {
-      this.props.hikesActions.resetHike();
-    },
-
-    componentWillReceiveProps({ params: { dashedName } }) {
-      if (this.props.params.dashedName !== dashedName) {
-        this.props.hikesActions.resetHike();
-      }
-    },
-
-    renderBody(showQuestions) {
-      if (showQuestions) {
-        return <Questions />;
-      }
-      return <Lecture />;
-    },
-
-    render() {
-      const {
-        currentHike: { title } = {},
-        showQuestions
-      } = this.props;
-
-      return (
-        <Col xs={ 12 }>
-          <Row>
-            <header className='text-center'>
-              <h4>{ title }</h4>
-            </header>
-            <hr />
-            <div className='spacer' />
-            <section
-              className={ 'text-center' }
-              title={ title }>
-              { this.renderBody(showQuestions) }
-            </section>
-          </Row>
-        </Col>
-      );
-    }
-  })
+const mapStateToProps = createSelector(
+  state => state.hikesApp.hikes.entities,
+  state => state.hikesApp.currentHike,
+  (hikes, currentHikeDashedName) => {
+    const currentHike = hikes[currentHikeDashedName];
+    return {
+      title: currentHike.title
+    };
+  }
 );
+// export plain component for testing
+export class Hike extends React.Component {
+  static displayName = 'Hike';
+
+  static propTypes = {
+    title: PropTypes.object,
+    params: PropTypes.object,
+    resetHike: PropTypes.func,
+    showQuestions: PropTypes.bool
+  };
+
+  componentWillUnmount() {
+    this.props.resetHike();
+  }
+
+  componentWillReceiveProps({ params: { dashedName } }) {
+    if (this.props.params.dashedName !== dashedName) {
+      this.props.resetHike();
+    }
+  }
+
+  renderBody(showQuestions) {
+    if (showQuestions) {
+      return <Questions />;
+    }
+    return <Lecture />;
+  }
+
+  render() {
+    const {
+      title,
+      showQuestions
+    } = this.props;
+
+    return (
+      <Col xs={ 12 }>
+        <Row>
+          <header className='text-center'>
+            <h4>{ title }</h4>
+          </header>
+          <hr />
+          <div className='spacer' />
+          <section
+            className={ 'text-center' }
+            title={ title }>
+            { this.renderBody(showQuestions) }
+          </section>
+        </Row>
+      </Col>
+    );
+  }
+}
+
+// export redux aware component
+export default connect(mapStateToProps, { resetHike });
diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx
index ba5a324..17c027a 100644
--- a/common/app/routes/Hikes/components/Hikes.jsx
+++ b/common/app/routes/Hikes/components/Hikes.jsx
@@ -1,74 +1,83 @@
 import React, { PropTypes } from 'react';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
 import { Row } from 'react-bootstrap';
-import { contain } from 'thundercats-react';
-// import debugFactory from 'debug';
+import shouldComponentUpdate from 'react-pure-render/function';
+import { createSelector } from 'reselect';
+// import debug from 'debug';
 
 import HikesMap from './Map.jsx';
+import { updateTitle } from '../../../redux/actions';
+import { fetchHikes } from '../redux/actions';
 
-// const debug = debugFactory('freecc:hikes');
+import contain from '../../../utils/professor-x';
 
-export default contain(
-  {
-    store: 'appStore',
-    map(state) {
-      return state.hikesApp;
-    },
-    actions: ['appActions'],
-    fetchAction: 'hikesActions.fetchHikes',
-    getPayload: ({ hikes, params }) => ({
-      isPrimed: (hikes && !!hikes.length),
-      dashedName: params.dashedName
-    }),
-    shouldContainerFetch(props, nextProps) {
-      return props.params.dashedName !== nextProps.params.dashedName;
+// const log = debug('fcc:hikes');
+
+const mapStateToProps = createSelector(
+  state => state.hikesApp.hikes,
+  hikes => {
+    if (!hikes || !hikes.entities || !hikes.results) {
+      return { hikes: [] };
     }
-  },
-  React.createClass({
-    displayName: 'Hikes',
+    return {
+      hikes: hikes.results.map(dashedName => hikes.enitites[dashedName])
+    };
+  }
+);
+const fetchOptions = {
+  fetchAction: 'fetchHikes',
 
-    propTypes: {
-      appActions: PropTypes.object,
-      children: PropTypes.element,
-      currentHike: PropTypes.object,
-      hikes: PropTypes.array,
-      params: PropTypes.object,
-      showQuestions: PropTypes.bool
-    },
+  isPrimed: ({ hikes }) => hikes && !!hikes.length,
+  getPayload: ({ params: { dashedName } }) => dashedName,
+  shouldContainerFetch(props, nextProps) {
+    return props.params.dashedName !== nextProps.params.dashedName;
+  }
+};
 
-    componentWillMount() {
-      const { appActions } = this.props;
-      appActions.setTitle('Videos');
-    },
+export class Hikes extends React.Component {
+  static displayName = 'Hikes';
 
-    renderMap(hikes) {
-      return (
-        <HikesMap hikes={ hikes }/>
-      );
-    },
+  static propTypes = {
+    children: PropTypes.element,
+    hikes: PropTypes.array,
+    params: PropTypes.object,
+    updateTitle: PropTypes.func
+  };
 
-    renderChild({ children, ...props }) {
-      if (!children) {
-        return null;
-      }
-      return React.cloneElement(children, props);
-    },
+  componentWillMount() {
+    const { updateTitle } = this.props;
+    updateTitle('Hikes');
+  }
 
-    render() {
-      const { hikes } = this.props;
-      const { dashedName } = this.props.params;
-      const preventOverflow = { overflow: 'hidden' };
-      return (
-        <div>
-          <Row style={ preventOverflow }>
-            {
-              // render sub-route
-              this.renderChild({ ...this.props, dashedName }) ||
-              // if no sub-route render hikes map
-              this.renderMap(hikes)
-            }
-          </Row>
-        </div>
-      );
-    }
-  })
-);
+  shouldComponentUpdate = shouldComponentUpdate;
+
+  renderMap(hikes) {
+    return (
+      <HikesMap hikes={ hikes }/>
+    );
+  }
+
+  render() {
+    const { hikes } = this.props;
+    const preventOverflow = { overflow: 'hidden' };
+    return (
+      <div>
+        <Row style={ preventOverflow }>
+          {
+            // render sub-route
+            this.props.children ||
+            // if no sub-route render hikes map
+            this.renderMap(hikes)
+          }
+        </Row>
+      </div>
+    );
+  }
+}
+
+// export redux and fetch aware component
+export default compose(
+  connect(mapStateToProps, { fetchHikes, updateTitle }),
+  contain(fetchOptions)
+)(Hikes);
diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx
index 09cea7a..1aac07f 100644
--- a/common/app/routes/Hikes/components/Lecture.jsx
+++ b/common/app/routes/Hikes/components/Lecture.jsx
@@ -1,95 +1,93 @@
 import React, { PropTypes } from 'react';
-import { contain } from 'thundercats-react';
+import { connect } from 'react-redux';
 import { Button, Col, Row } from 'react-bootstrap';
-import { History } from 'react-router';
 import Vimeo from 'react-vimeo';
-import debugFactory from 'debug';
+import { createSelector } from 'reselect';
+import debug from 'debug';
 
-const debug = debugFactory('freecc:hikes');
+const log = debug('fcc:hikes');
 
-export default contain(
-  {
-    actions: ['hikesActions'],
-    store: 'appStore',
-    map(state) {
-      const {
-        currentHike: {
-          dashedName,
-          description,
-          challengeSeed: [id] = [0]
-        } = {}
-      } = state.hikesApp;
+const mapStateToProps = createSelector(
+  state => state.hikesApp.hikes.entities,
+  state => state.hikesApp.currentHike,
+  (hikes, currentHikeDashedName) => {
+    const currentHike = hikes[currentHikeDashedName];
+    const {
+      dashedName,
+      description,
+      challengeSeed: [id] = [0]
+    } = currentHike || {};
+    return {
+      id,
+      dashedName,
+      description
+    };
+  }
+);
 
-      return {
-        dashedName,
-        description,
-        id
-      };
-    }
-  },
-  React.createClass({
-    displayName: 'Lecture',
-    mixins: [History],
+export class Lecture extends React.Component {
+  static displayName = 'Lecture';
 
-    propTypes: {
-      dashedName: PropTypes.string,
-      description: PropTypes.array,
-      id: PropTypes.string,
-      hikesActions: PropTypes.object
-    },
+  static propTypes = {
+    dashedName: PropTypes.string,
+    description: PropTypes.array,
+    id: PropTypes.string,
+    hikesActions: PropTypes.object
+  };
 
-    shouldComponentUpdate(nextProps) {
-      const { props } = this;
-      return nextProps.id !== props.id;
-    },
+  shouldComponentUpdate(nextProps) {
+    const { props } = this;
+    return nextProps.id !== props.id;
+  }
 
-    handleError: debug,
+  handleError: log;
 
-    handleFinish(hikesActions) {
-      debug('loading questions');
-      hikesActions.toggleQuestions();
-    },
+  handleFinish(hikesActions) {
+    debug('loading questions');
+    hikesActions.toggleQuestions();
+  }
 
-    renderTranscript(transcript, dashedName) {
-      return transcript.map((line, index) => (
-        <p
-          className='lead text-left'
-          key={ dashedName + index }>
-          { line }
-        </p>
-      ));
-    },
+  renderTranscript(transcript, dashedName) {
+    return transcript.map((line, index) => (
+      <p
+        className='lead text-left'
+        key={ dashedName + index }>
+        { line }
+      </p>
+    ));
+  }
 
-    render() {
-      const {
-        id = '1',
-        description = [],
-        hikesActions
-      } = this.props;
-      const dashedName = 'foo';
+  render() {
+    const {
+      id = '1',
+      description = [],
+      hikesActions
+    } = this.props;
+    const dashedName = 'foo';
 
-      return (
-        <Col xs={ 12 }>
-          <Row>
-            <Vimeo
-              onError={ this.handleError }
-              onFinish= { () => this.handleFinish(hikesActions) }
-              videoId={ id } />
-          </Row>
-          <Row>
-            <article>
-              { this.renderTranscript(description, dashedName) }
-            </article>
-            <Button
-              block={ true }
-              bsSize='large'
-              bsStyle='primary'
-              onClick={ () => this.handleFinish(hikesActions) }>
-              Take me to the Questions
-            </Button>
-          </Row>
-        </Col>
-      );
-    }
-  })
-);
+    return (
+      <Col xs={ 12 }>
+        <Row>
+          <Vimeo
+            onError={ this.handleError }
+            onFinish= { () => this.handleFinish(hikesActions) }
+            videoId={ id } />
+        </Row>
+        <Row>
+          <article>
+            { this.renderTranscript(description, dashedName) }
+          </article>
+          <Button
+            block={ true }
+            bsSize='large'
+            bsStyle='primary'
+            onClick={ () => this.handleFinish(hikesActions) }>
+            Take me to the Questions
+          </Button>
+        </Row>
+      </Col>
+    );
+  }
+}
+
+export default connect(mapStateToProps, { })(Lecture);
diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js
deleted file mode 100644
index 5395c23..0000000
--- a/common/app/routes/Hikes/flux/Actions.js
+++ /dev/null
@@ -1,361 +0,0 @@
-import _ from 'lodash';
-import { Observable } from 'rx';
-import { Actions } from 'thundercats';
-import debugFactory from 'debug';
-
-const debug = debugFactory('freecc:hikes:actions');
-const noOp = { transform: () => {} };
-
-function getCurrentHike(hikes = [{}], dashedName, currentHike) {
-  if (!dashedName) {
-    debug('no dashedName');
-    return hikes[0];
-  }
-
-  const filterRegex = new RegExp(dashedName, 'i');
-  if (currentHike && filterRegex.test(currentHike.dashedName)) {
-    return currentHike;
-  }
-
-  debug('setting new hike');
-  return hikes
-    .filter(({ dashedName }) => {
-      return filterRegex.test(dashedName);
-    })
-    .reduce((throwAway, hike) => {
-      return hike;
-    }, currentHike || {});
-}
-
-function findNextHike(hikes, id) {
-  if (!id) {
-    debug('find next hike no id provided');
-    return hikes[0];
-  }
-  const currentIndex = _.findIndex(hikes, ({ id: _id }) => _id === id);
-  return hikes[currentIndex + 1] || hikes[0];
-}
-
-
-function getMouse(e, [dx, dy]) {
-  let { pageX, pageY, touches, changedTouches } = e;
-
-  // touches can be empty on touchend
-  if (touches || changedTouches) {
-    e.preventDefault();
-    // these re-assigns the values of pageX, pageY from touches
-    ({ pageX, pageY } = touches[0] || changedTouches[0]);
-  }
-
-  return [pageX - dx, pageY - dy];
-}
-
-export default Actions({
-  refs: { displayName: 'HikesActions' },
-  shouldBindMethods: true,
-  fetchHikes({ isPrimed, dashedName }) {
-    if (isPrimed) {
-      return {
-        transform: (state) => {
-
-          const { hikesApp: oldState } = state;
-          const currentHike = getCurrentHike(
-            oldState.hikes,
-            dashedName,
-            oldState.currentHike
-          );
-
-          const hikesApp = { ...oldState, currentHike };
-          return Object.assign({}, state, { hikesApp });
-        }
-      };
-    }
-
-    return this.readService$('hikes', null, null)
-      .map(hikes => {
-        const currentHike = getCurrentHike(hikes, dashedName);
-        return {
-          transform(state) {
-            const hikesApp = { ...state.hikesApp, currentHike, hikes };
-            return { ...state, hikesApp };
-          }
-        };
-      })
-      .catch(err => Observable.just({
-        transform(state) { return { ...state, err }; }
-      }));
-  },
-
-  toggleQuestions() {
-    return {
-      transform(state) {
-        const hikesApp = {
-          ...state.hikesApp,
-          showQuestions: !state.hikesApp.showQuestions,
-          currentQuestion: 1
-        };
-        return { ...state, hikesApp };
-      }
-    };
-  },
-
-  grabQuestion(e) {
-    let { pageX, pageY, touches } = e;
-    if (touches) {
-      e.preventDefault();
-      // these re-assigns the values of pageX, pageY from touches
-      ({ pageX, pageY } = touches[0]);
-    }
-    const delta = [pageX, pageY];
-    const mouse = [0, 0];
-
-    return {
-      transform(state) {
-        return {
-          ...state,
-          hikesApp: {
-            ...state.hikesApp,
-            isPressed: true,
-            delta,
-            mouse
-          }
-        };
-      }
-    };
-  },
-
-  releaseQuestion() {
-    return {
-      transform(state) {
-        return {
-          ...state,
-          hikesApp: {
-            ...state.hikesApp,
-            isPressed: false,
-            mouse: [0, 0]
-          }
-        };
-      }
-    };
-  },
-
-  moveQuestion({ e, delta }) {
-    const mouse = getMouse(e, delta);
-
-    return {
-      transform(state) {
-        return {
-          ...state,
-          hikesApp: {
-            ...state.hikesApp,
-            mouse
-          }
-        };
-      }
-    };
-  },
-
-  answer({
-    e,
-    answer,
-    userAnswer,
-    hike: { id, name, tests, challengeType },
-    currentQuestion,
-    isSignedIn,
-    delta,
-    info,
-    threshold
-  }) {
-    if (typeof userAnswer === 'undefined') {
-      const [positionX] = getMouse(e, delta);
-
-      // question released under threshold
-      if (Math.abs(positionX) < threshold) {
-        return noOp;
-      }
-
-      if (positionX >= threshold) {
-        userAnswer = true;
-      }
-
-      if (positionX <= -threshold) {
-        userAnswer = false;
-      }
-    }
-
-    // incorrect question
-    if (answer !== userAnswer) {
-      const startShake = {
-        transform(state) {
-          const toast = !info ?
-            state.toast :
-            {
-              id: state.toast && state.toast.id ? state.toast.id + 1 : 1,
-              title: 'Hint',
-              message: info,
-              type: 'info'
-            };
-
-          return {
-            ...state,
-            toast,
-            hikesApp: {
-              ...state.hikesApp,
-              shake: true
-            }
-          };
-        }
-      };
-
-      const removeShake = {
-        transform(state) {
-          return {
-            ...state,
-            hikesApp: {
-              ...state.hikesApp,
-              shake: false
-            }
-          };
-        }
-      };
-
-      return Observable
-        .just(removeShake)
-        .delay(500)
-        .startWith(startShake);
-    }
-
-    // move to next question
-    // index 0
-    if (tests[currentQuestion]) {
-
-      return Observable.just({
-        transform(state) {
-          const hikesApp = {
-            ...state.hikesApp,
-            mouse: [0, 0]
-          };
-          return { ...state, hikesApp };
-        }
-      })
-        .delay(300)
-        .startWith({
-          transform(state) {
-
-            const hikesApp = {
-              ...state.hikesApp,
-              currentQuestion: currentQuestion + 1,
-              mouse: [ userAnswer ? 1000 : -1000, 0],
-              isPressed: false
-            };
-
-            return { ...state, hikesApp };
-          }
-        });
-    }
-
-    // challenge completed
-    let update$;
-    if (isSignedIn) {
-      const body = { id, name, challengeType: +challengeType };
-      update$ = this.postJSON$('/completed-challenge', body)
-        // if post fails, will retry once
-        .retry(3)
-        .map(({ alreadyCompleted, points }) => ({
-          transform(state) {
-            return {
-              ...state,
-              points,
-              toast: {
-                message:
-                  'Challenge saved.' +
-                  (alreadyCompleted ? '' : ' First time Completed!'),
-                title: 'Saved',
-                type: 'info',
-                id: state.toast && state.toast.id ? state.toast.id + 1 : 1
-              }
-            };
-          }
-        }))
-        .catch((errObj => {
-          const err = new Error(errObj.message);
-          err.stack = errObj.stack;
-          return {
-            transform(state) { return { ...state, err }; }
-          };
-        }));
-    } else {
-      update$ = Observable.just({ transform: (() => {}) });
-    }
-
-    const challengeCompleted$ = Observable.just({
-      transform(state) {
-        const { hikes, currentHike: { id } } = state.hikesApp;
-        const currentHike = findNextHike(hikes, id);
-
-        return {
-          ...state,
-          points: isSignedIn ? state.points + 1 : state.points,
-          hikesApp: {
-            ...state.hikesApp,
-            currentHike,
-            showQuestions: false,
-            currentQuestion: 1,
-            mouse: [0, 0]
-          },
-          toast: {
-            title: 'Congratulations!',
-            message: 'Challenge completed.' + (isSignedIn ? ' Saving...' : ''),
-            id: state.toast && state.toast.id ?
-              state.toast.id + 1 :
-              1,
-            type: 'success'
-          },
-          location: {
-            action: 'PUSH',
-            pathname: currentHike && currentHike.dashedName ?
-              `/videos/${ currentHike.dashedName }` :
-              '/videos'
-          }
-        };
-      }
-    });
-
-    const correctAnswer = {
-      transform(state) {
-        return {
-          ...state,
-          hikesApp: {
-            ...state.hikesApp,
-            isCorrect: true,
-            isPressed: false,
-            delta: [0, 0],
-            mouse: [ userAnswer ? 1000 : -1000, 0]
-          }
-        };
-      }
-    };
-
-    return Observable.merge(challengeCompleted$, update$)
-      .delay(300)
-      .startWith(correctAnswer)
-      .catch(err => Observable.just({
-        transform(state) { return { ...state, err }; }
-      }));
-  },
-  resetHike() {
-    return {
-      transform(state) {
-        return { ...state,
-          hikesApp: {
-            ...state.hikesApp,
-            currentQuestion: 1,
-            showQuestions: false,
-            mouse: [0, 0],
-            delta: [0, 0]
-          }
-        };
-      }
-    };
-  }
-});
diff --git a/common/app/routes/Hikes/flux/index.js b/common/app/routes/Hikes/flux/index.js
deleted file mode 100644
index 0936f32..0000000
--- a/common/app/routes/Hikes/flux/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default from './Actions';
diff --git a/common/app/routes/Hikes/index.js b/common/app/routes/Hikes/index.js
index fa8ba78..b759608 100644
--- a/common/app/routes/Hikes/index.js
+++ b/common/app/routes/Hikes/index.js
@@ -1,11 +1,6 @@
 import Hikes from './components/Hikes.jsx';
 import Hike from './components/Hike.jsx';
 
-/*
- * show video /hikes/someVideo
- * show question /hikes/someVideo/question1
- */
-
 export default {
   path: 'videos',
   component: Hikes,
diff --git a/common/app/routes/Hikes/redux/actions.js b/common/app/routes/Hikes/redux/actions.js
new file mode 100644
index 0000000..a2aec23
--- /dev/null
+++ b/common/app/routes/Hikes/redux/actions.js
@@ -0,0 +1,54 @@
+import { createAction } from 'redux-actions';
+
+import types from './types';
+import { getMouse } from './utils';
+
+
+// fetchHikes(dashedName?: String) => Action
+// used with fetchHikesSaga
+export const fetchHikes = createAction(types.fetchHikes);
+// fetchHikesCompleted(hikes: Object) => Action
+// hikes is a normalized response from server
+// called within fetchHikesSaga
+export const fetchHikesCompleted = createAction(
+  types.fetchHikesCompleted,
+  (hikes, currentHike) => ({ hikes, currentHike })
+);
+
+export const toggleQuestion = createAction(types.toggleQuestion);
+
+export const grabQuestions = createAction(types.grabQuestions, e => {
+  let { pageX, pageY, touches } = e;
+  if (touches) {
+    e.preventDefault();
+    // these re-assigns the values of pageX, pageY from touches
+    ({ pageX, pageY } = touches[0]);
+  }
+  const delta = [pageX, pageY];
+  const mouse = [0, 0];
+
+  return { delta, mouse };
+});
+
+export const releaseQuestion = createAction(types.releaseQuestions);
+export const moveQuestion = createAction(
+  types.moveQuestion,
+  ({ e, delta }) => getMouse(e, delta)
+);
+
+// answer({
+//   e: Event,
+//   answer: Boolean,
+//   userAnswer: Boolean,
+//   info: String,
+//   threshold: Number
+// }) => Action
+export const answer = createAction(types.answer);
+
+export const startShake = createAction(types.startShake);
+export const endShake = createAction(types.primeNextQuestion);
+
+export const goToNextQuestion = createAction(types.goToNextQuestion);
+
+export const hikeCompleted = createAction(types.hikeCompleted);
+export const goToNextHike = createAction(types.goToNextHike);
diff --git a/common/app/routes/Hikes/redux/answer-saga.js b/common/app/routes/Hikes/redux/answer-saga.js
new file mode 100644
index 0000000..0c4cd39
--- /dev/null
+++ b/common/app/routes/Hikes/redux/answer-saga.js
@@ -0,0 +1,128 @@
+import { Observable } from 'rx';
+// import { routeActions } from 'react-simple-router';
+
+import types from './types';
+import { getMouse } from './utils';
+
+import { makeToast, updatePoints } from '../../../redux/actions';
+import { hikeCompleted, goToNextHike } from './actions';
+import { postJSON$ } from '../../../../utils/ajax-stream';
+
+export default () => ({ getState, dispatch }) => next => {
+  return function answerSaga(action) {
+    if (types.answer !== action.type) {
+      return next(action);
+    }
+
+    const {
+      e,
+      answer,
+      userAnswer,
+      info,
+      threshold
+    } = action.payload;
+
+    const {
+      app: { isSignedIn },
+      hikesApp: {
+        currentQuestion,
+        currentHike: { id, name, challengeType },
+        tests = [],
+        delta = [ 0, 0 ]
+      }
+    } = getState();
+
+    let finalAnswer;
+    // drag answer, compute response
+    if (typeof userAnswer === 'undefined') {
+      const [positionX] = getMouse(e, delta);
+
+      // question released under threshold
+      if (Math.abs(positionX) < threshold) {
+        return next(action);
+      }
+
+      if (positionX >= threshold) {
+        finalAnswer = true;
+      }
+
+      if (positionX <= -threshold) {
+        finalAnswer = false;
+      }
+    } else {
+      finalAnswer = userAnswer;
+    }
+
+    // incorrect question
+    if (answer !== finalAnswer) {
+      if (info) {
+        dispatch({
+          type: 'makeToast',
+          payload: {
+            title: 'Hint',
+            message: info,
+            type: 'info'
+          }
+        });
+      }
+
+      return Observable
+        .just({ type: types.removeShake })
+        .delay(500)
+        .startWith({ type: types.startShake })
+        .doOnNext(dispatch);
+    }
+
+    if (tests[currentQuestion]) {
+      return Observable
+        .just({ type: types.goToNextQuestion })
+        .delay(300)
+        .startWith({ type: types.primeNextQuestion });
+    }
+
+    let updateUser$;
+    if (isSignedIn) {
+      const body = { id, name, challengeType };
+      updateUser$ = postJSON$('/completed-challenge', body)
+        // if post fails, will retry once
+        .retry(3)
+        .flatMap(({ alreadyCompleted, points }) => {
+          return Observable.of(
+            makeToast({
+                message:
+                  'Challenge saved.' +
+                  (alreadyCompleted ? '' : ' First time Completed!'),
+                title: 'Saved',
+                type: 'info'
+            }),
+            updatePoints(points),
+          );
+        })
+        .catch(error => {
+          return Observable.just({
+            type: 'error',
+            error
+          });
+        });
+    } else {
+      updateUser$ = Observable.empty();
+    }
+
+    const challengeCompleted$ = Observable.of(
+      goToNextHike(),
+      makeToast({
+        title: 'Congratulations!',
+        message: 'Hike completed.' + (isSignedIn ? ' Saving...' : ''),
+        type: 'success'
+      })
+    );
+
+    return Observable.merge(challengeCompleted$, updateUser$)
+      .delay(300)
+      .startWith(hikeCompleted(finalAnswer))
+      .catch(error => Observable.just({
+        type: 'error',
+        error
+      }));
+  };
+};
diff --git a/common/app/routes/Hikes/redux/fetch-hikes-saga.js b/common/app/routes/Hikes/redux/fetch-hikes-saga.js
new file mode 100644
index 0000000..07d482f
--- /dev/null
+++ b/common/app/routes/Hikes/redux/fetch-hikes-saga.js
@@ -0,0 +1,46 @@
+import { Observable } from 'rx';
+import { normalize, Schema, arrayOf } from 'normalizr';
+// import debug from 'debug';
+
+import types from './types';
+import { fetchHikesCompleted } from './actions';
+import { handleError } from '../../../redux/types';
+
+import { getCurrentHike } from './utils';
+
+// const log = debug('fcc:fetch-hikes-saga');
+const hike = new Schema('hike', { idAttribute: 'dashedName' });
+
+export default ({ services }) => ({ dispatch }) => next => {
+  return function fetchHikesSaga(action) {
+    if (action.type !== types.fetchHikes) {
+      return next(action);
+    }
+
+    const dashedName = action.payload;
+    return services.readService$({ service: 'hikes' })
+      .map(hikes => {
+        const { entities, result } = normalize(
+          { hikes },
+          { hikes: arrayOf(hike) }
+        );
+
+        hikes = {
+          entities: entities.hike,
+          results: result.hikes
+        };
+
+        const currentHike = getCurrentHike(hikes, dashedName);
+
+        console.log('foo', currentHike);
+        return fetchHikesCompleted(hikes, currentHike);
+      })
+      .catch(error => {
+        return Observable.just({
+          type: handleError,
+          error
+        });
+      })
+      .doOnNext(dispatch);
+  };
+};
diff --git a/common/app/routes/Hikes/redux/index.js b/common/app/routes/Hikes/redux/index.js
new file mode 100644
index 0000000..8d94299
--- /dev/null
+++ b/common/app/routes/Hikes/redux/index.js
@@ -0,0 +1,8 @@
+export actions from './actions';
+export reducer from './reducer';
+export types from './types';
+
+import answerSaga from './answer-saga';
+import fetchHikesSaga from './fetch-hikes-saga';
+
+export const sagas = [ answerSaga, fetchHikesSaga ];
diff --git a/common/app/routes/Hikes/redux/oldActions.js b/common/app/routes/Hikes/redux/oldActions.js
new file mode 100644
index 0000000..281306f
--- /dev/null
+++ b/common/app/routes/Hikes/redux/oldActions.js
@@ -0,0 +1,361 @@
+import _ from 'lodash';
+import { Observable } from 'rx';
+import { Actions } from 'thundercats';
+import debugFactory from 'debug';
+
+const debug = debugFactory('fcc:hikes:actions');
+const noOp = { transform: () => {} };
+
+function getCurrentHike(hikes = [{}], dashedName, currentHike) {
+  if (!dashedName) {
+    debug('no dashedName');
+    return hikes[0];
+  }
+
+  const filterRegex = new RegExp(dashedName, 'i');
+  if (currentHike && filterRegex.test(currentHike.dashedName)) {
+    return currentHike;
+  }
+
+  debug('setting new hike');
+  return hikes
+    .filter(({ dashedName }) => {
+      return filterRegex.test(dashedName);
+    })
+    .reduce((throwAway, hike) => {
+      return hike;
+    }, currentHike || {});
+}
+
+function findNextHike(hikes, id) {
+  if (!id) {
+    debug('find next hike no id provided');
+    return hikes[0];
+  }
+  const currentIndex = _.findIndex(hikes, ({ id: _id }) => _id === id);
+  return hikes[currentIndex + 1] || hikes[0];
+}
+
+
+function getMouse(e, [dx, dy]) {
+  let { pageX, pageY, touches, changedTouches } = e;
+
+  // touches can be empty on touchend
+  if (touches || changedTouches) {
+    e.preventDefault();
+    // these re-assigns the values of pageX, pageY from touches
+    ({ pageX, pageY } = touches[0] || changedTouches[0]);
+  }
+
+  return [pageX - dx, pageY - dy];
+}
+
+export default Actions({
+  refs: { displayName: 'HikesActions' },
+  shouldBindMethods: true,
+  fetchHikes({ isPrimed, dashedName }) {
+    if (isPrimed) {
+      return {
+        transform: (state) => {
+
+          const { hikesApp: oldState } = state;
+          const currentHike = getCurrentHike(
+            oldState.hikes,
+            dashedName,
+            oldState.currentHike
+          );
+
+          const hikesApp = { ...oldState, currentHike };
+          return Object.assign({}, state, { hikesApp });
+        }
+      };
+    }
+
+    return this.readService$('hikes', null, null)
+      .map(hikes => {
+        const currentHike = getCurrentHike(hikes, dashedName);
+        return {
+          transform(state) {
+            const hikesApp = { ...state.hikesApp, currentHike, hikes };
+            return { ...state, hikesApp };
+          }
+        };
+      })
+      .catch(err => Observable.just({
+        transform(state) { return { ...state, err }; }
+      }));
+  },
+
+  toggleQuestions() {
+    return {
+      transform(state) {
+        const hikesApp = {
+          ...state.hikesApp,
+          showQuestions: !state.hikesApp.showQuestions,
+          currentQuestion: 1
+        };
+        return { ...state, hikesApp };
+      }
+    };
+  },
+
+  grabQuestion(e) {
+    let { pageX, pageY, touches } = e;
+    if (touches) {
+      e.preventDefault();
+      // these re-assigns the values of pageX, pageY from touches
+      ({ pageX, pageY } = touches[0]);
+    }
+    const delta = [pageX, pageY];
+    const mouse = [0, 0];
+
+    return {
+      transform(state) {
+        return {
+          ...state,
+          hikesApp: {
+            ...state.hikesApp,
+            isPressed: true,
+            delta,
+            mouse
+          }
+        };
+      }
+    };
+  },
+
+  releaseQuestion() {
+    return {
+      transform(state) {
+        return {
+          ...state,
+          hikesApp: {
+            ...state.hikesApp,
+            isPressed: false,
+            mouse: [0, 0]
+          }
+        };
+      }
+    };
+  },
+
+  moveQuestion({ e, delta }) {
+    const mouse = getMouse(e, delta);
+
+    return {
+      transform(state) {
+        return {
+          ...state,
+          hikesApp: {
+            ...state.hikesApp,
+            mouse
+          }
+        };
+      }
+    };
+  },
+
+  answer({
+    e,
+    answer,
+    userAnswer,
+    hike: { id, name, tests, challengeType },
+    currentQuestion,
+    isSignedIn,
+    delta,
+    info,
+    threshold
+  }) {
+    if (typeof userAnswer === 'undefined') {
+      const [positionX] = getMouse(e, delta);
+
+      // question released under threshold
+      if (Math.abs(positionX) < threshold) {
+        return noOp;
+      }
+
+      if (positionX >= threshold) {
+        userAnswer = true;
+      }
+
+      if (positionX <= -threshold) {
+        userAnswer = false;
+      }
+    }
+
+    // incorrect question
+    if (answer !== userAnswer) {
+      const startShake = {
+        transform(state) {
+          const toast = !info ?
+            state.toast :
+            {
+              id: state.toast && state.toast.id ? state.toast.id + 1 : 1,
+              title: 'Hint',
+              message: info,
+              type: 'info'
+            };
+
+          return {
+            ...state,
+            toast,
+            hikesApp: {
+              ...state.hikesApp,
+              shake: true
+            }
+          };
+        }
+      };
+
+      const removeShake = {
+        transform(state) {
+          return {
+            ...state,
+            hikesApp: {
+              ...state.hikesApp,
+              shake: false
+            }
+          };
+        }
+      };
+
+      return Observable
+        .just(removeShake)
+        .delay(500)
+        .startWith(startShake);
+    }
+
+    // move to next question
+    // index 0
+    if (tests[currentQuestion]) {
+
+      return Observable.just({
+        transform(state) {
+          const hikesApp = {
+            ...state.hikesApp,
+            mouse: [0, 0]
+          };
+          return { ...state, hikesApp };
+        }
+      })
+        .delay(300)
+        .startWith({
+          transform(state) {
+
+            const hikesApp = {
+              ...state.hikesApp,
+              currentQuestion: currentQuestion + 1,
+              mouse: [ userAnswer ? 1000 : -1000, 0],
+              isPressed: false
+            };
+
+            return { ...state, hikesApp };
+          }
+        });
+    }
+
+    // challenge completed
+    let update$;
+    if (isSignedIn) {
+      const body = { id, name, challengeType: +challengeType };
+      update$ = this.postJSON$('/completed-challenge', body)
+        // if post fails, will retry once
+        .retry(3)
+        .map(({ alreadyCompleted, points }) => ({
+          transform(state) {
+            return {
+              ...state,
+              points,
+              toast: {
+                message:
+                  'Challenge saved.' +
+                  (alreadyCompleted ? '' : ' First time Completed!'),
+                title: 'Saved',
+                type: 'info',
+                id: state.toast && state.toast.id ? state.toast.id + 1 : 1
+              }
+            };
+          }
+        }))
+        .catch((errObj => {
+          const err = new Error(errObj.message);
+          err.stack = errObj.stack;
+          return {
+            transform(state) { return { ...state, err }; }
+          };
+        }));
+    } else {
+      update$ = Observable.just({ transform: (() => {}) });
+    }
+
+    const challengeCompleted$ = Observable.just({
+      transform(state) {
+        const { hikes, currentHike: { id } } = state.hikesApp;
+        const currentHike = findNextHike(hikes, id);
+
+        return {
+          ...state,
+          points: isSignedIn ? state.points + 1 : state.points,
+          hikesApp: {
+            ...state.hikesApp,
+            currentHike,
+            showQuestions: false,
+            currentQuestion: 1,
+            mouse: [0, 0]
+          },
+          toast: {
+            title: 'Congratulations!',
+            message: 'Challenge completed.' + (isSignedIn ? ' Saving...' : ''),
+            id: state.toast && state.toast.id ?
+              state.toast.id + 1 :
+              1,
+            type: 'success'
+          },
+          location: {
+            action: 'PUSH',
+            pathname: currentHike && currentHike.dashedName ?
+              `/videos/${ currentHike.dashedName }` :
+              '/videos'
+          }
+        };
+      }
+    });
+
+    const correctAnswer = {
+      transform(state) {
+        return {
+          ...state,
+          hikesApp: {
+            ...state.hikesApp,
+            isCorrect: true,
+            isPressed: false,
+            delta: [0, 0],
+            mouse: [ userAnswer ? 1000 : -1000, 0]
+          }
+        };
+      }
+    };
+
+    return Observable.merge(challengeCompleted$, update$)
+      .delay(300)
+      .startWith(correctAnswer)
+      .catch(err => Observable.just({
+        transform(state) { return { ...state, err }; }
+      }));
+  },
+  resetHike() {
+    return {
+      transform(state) {
+        return { ...state,
+          hikesApp: {
+            ...state.hikesApp,
+            currentQuestion: 1,
+            showQuestions: false,
+            mouse: [0, 0],
+            delta: [0, 0]
+          }
+        };
+      }
+    };
+  }
+});
diff --git a/common/app/routes/Hikes/redux/reducer.js b/common/app/routes/Hikes/redux/reducer.js
new file mode 100644
index 0000000..e6a49f0
--- /dev/null
+++ b/common/app/routes/Hikes/redux/reducer.js
@@ -0,0 +1,88 @@
+import { handleActions } from 'redux-actions';
+import types from './types';
+import { findNextHike } from './utils';
+
+const initialState = {
+  hikes: {
+    results: [],
+    entities: {}
+  },
+  // lecture state
+  currentHike: '',
+  showQuestions: false
+};
+
+export default handleActions(
+  {
+    [types.toggleQuestion]: state => ({
+      ...state,
+      showQuestions: !state.showQuestions,
+      currentQuestion: 1
+    }),
+
+    [types.grabQuestion]: (state, { payload: { delta, mouse } }) => ({
+      ...state,
+      isPressed: true,
+      delta,
+      mouse
+    }),
+
+    [types.releaseQuestion]: state => ({
+      ...state,
+      isPressed: false,
+      mouse: [ 0, 0 ]
+    }),
+
+    [types.moveQuestion]: (state, { payload: mouse }) => ({ ...state, mouse }),
+
+    [types.resetHike]: state => ({
+      ...state,
+      currentQuestion: 1,
+      showQuestions: false,
+      mouse: [0, 0],
+      delta: [0, 0]
+    }),
+
+    [types.startShake]: state => ({ ...state, shake: true }),
+    [types.endShake]: state => ({ ...state, shake: false }),
+
+    [types.primeNextQuestion]: (state, { payload: userAnswer }) => ({
+      ...state,
+      currentQuestion: state.currentQuestion + 1,
+      mouse: [ userAnswer ? 1000 : -1000, 0],
+      isPressed: false
+    }),
+
+    [types.goToNextQuestion]: state => ({
+      ...state,
+      mouse: [ 0, 0 ]
+    }),
+
+    [types.hikeCompleted]: (state, { payload: userAnswer } ) => ({
+      ...state,
+      isCorrect: true,
+      isPressed: false,
+      delta: [ 0, 0 ],
+      mouse: [ userAnswer ? 1000 : -1000, 0]
+    }),
+
+    [types.goToNextHike]: state => ({
+      ...state,
+      currentHike: findNextHike(state.hikes, state.currentHike.id),
+      showQuestions: false,
+      currentQuestion: 1,
+      mouse: [ 0, 0 ]
+    }),
+
+    [types.fetchHikesCompleted]: (state, { payload }) => {
+      const { hikes, currentHike } = payload;
+
+      return {
+        ...state,
+        hikes,
+        currentHike
+      };
+    }
+  },
+  initialState
+);
diff --git a/common/app/routes/Hikes/redux/types.js b/common/app/routes/Hikes/redux/types.js
new file mode 100644
index 0000000..c96ae0e
--- /dev/null
+++ b/common/app/routes/Hikes/redux/types.js
@@ -0,0 +1,23 @@
+const types = [
+  'fetchHikes',
+  'fetchHikesCompleted',
+
+  'toggleQuestionView',
+  'grabQuestion',
+  'releaseQuestion',
+  'moveQuestion',
+
+  'answer',
+
+  'startShake',
+  'endShake',
+
+  'primeNextQuestion',
+  'goToNextQuestion',
+
+  'hikeCompleted',
+  'goToNextHike'
+];
+
+export default types
+  .reduce((types, type) => ({ ...types, [type]: `videos.${type}` }), {});
diff --git a/common/app/routes/Hikes/redux/utils.js b/common/app/routes/Hikes/redux/utils.js
new file mode 100644
index 0000000..d03b33d
--- /dev/null
+++ b/common/app/routes/Hikes/redux/utils.js
@@ -0,0 +1,74 @@
+import debug from 'debug';
+import _ from 'lodash';
+
+const log = debug('fcc:hikes:utils');
+
+function getFirstHike(hikes) {
+  return hikes.results[0];
+}
+
+// interface Hikes {
+//   results: String[],
+//   entities: {
+//     hikeId: Challenge
+//   }
+// }
+//
+// findCurrentHike({
+//   hikes: Hikes,
+//   dashedName: String
+// }) => String
+export function findCurrentHike(hikes = {}, dashedName) {
+  if (!dashedName) {
+    return getFirstHike(hikes) || {};
+  }
+
+  const filterRegex = new RegExp(dashedName, 'i');
+
+  return hikes
+    .results
+    .filter(dashedName => {
+      return filterRegex.test(dashedName);
+    })
+    .reduce((throwAway, hike) => {
+      return hike;
+    }, {});
+}
+
+export function getCurrentHike(hikes = {}, dashedName) {
+  if (!dashedName) {
+    return getFirstHike(hikes) || {};
+  }
+  return hikes.entities[dashedName];
+}
+
+export function findNextHike({ entities, results }, dashedName) {
+  if (!dashedName) {
+    log('find next hike no id provided');
+    return entities[results[0]];
+  }
+  const currentIndex = _.findIndex(
+    results,
+    ({ dashedName: _dashedName }) => _dashedName === dashedName
+  );
+
+  if (currentIndex >= results.length) {
+    return '';
+  }
+
+  return entities[results[currentIndex + 1]];
+}
+
+
+export function getMouse(e, [dx, dy]) {
+  let { pageX, pageY, touches, changedTouches } = e;
+
+  // touches can be empty on touchend
+  if (touches || changedTouches) {
+    e.preventDefault();
+    // these re-assigns the values of pageX, pageY from touches
+    ({ pageX, pageY } = touches[0] || changedTouches[0]);
+  }
+
+  return [pageX - dx, pageY - dy];
+}
diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx
index 83aba5e..43f2dfb 100644
--- a/common/app/routes/Jobs/components/NewJob.jsx
+++ b/common/app/routes/Jobs/components/NewJob.jsx
@@ -26,7 +26,7 @@ import {
   isURL
 } from 'validator';
 
-const debug = debugFactory('freecc:jobs:newForm');
+const debug = debugFactory('fcc:jobs:newForm');
 
 const checkValidity = [
   'position',
diff --git a/common/app/sagas.js b/common/app/sagas.js
new file mode 100644
index 0000000..fc40bc3
--- /dev/null
+++ b/common/app/sagas.js
@@ -0,0 +1,6 @@
+import { sagas as appSagas } from './redux';
+import { sagas as hikesSagas} from './routes/Hikes/redux';
+export default [
+  ...appSagas,
+  ...hikesSagas
+];
diff --git a/common/app/temp.js b/common/app/temp.js
new file mode 100644
index 0000000..4ed6ed8
--- /dev/null
+++ b/common/app/temp.js
@@ -0,0 +1,40 @@
+import stamp from 'stampit';
+import { post$, postJSON$ } from '../utils/ajax-stream.js';
+
+  const serviceStamp = stamp({
+    methods: {
+      readService$(resource, params, config) {
+
+        return Observable.create(function(observer) {
+          services.read(resource, params, config, (err, res) => {
+            if (err) {
+              return observer.onError(err);
+            }
+
+            observer.onNext(res);
+            observer.onCompleted();
+          });
+
+          return Disposable.create(function() {
+            observer.dispose();
+          });
+        });
+      },
+      createService$(resource, params, body, config) {
+        return Observable.create(function(observer) {
+          services.create(resource, params, body, config, (err, res) => {
+            if (err) {
+              return observer.onError(err);
+            }
+
+            observer.onNext(res);
+            observer.onCompleted();
+          });
+
+          return Disposable.create(function() {
+            observer.dispose();
+          });
+        });
+      }
+    }
+  });
diff --git a/common/app/utils/Professor-Context.js b/common/app/utils/Professor-Context.js
new file mode 100644
index 0000000..0ba3133
--- /dev/null
+++ b/common/app/utils/Professor-Context.js
@@ -0,0 +1,42 @@
+import React, { Children, PropTypes } from 'react';
+
+class ProfessorContext extends React.Component {
+  constructor(props) {
+    super(props);
+    this.professor = props.professor;
+  }
+  static displayName = 'ProfessorContext';
+
+  static propTypes = {
+    professor: PropTypes.object,
+    children: PropTypes.element.isRequired
+  };
+
+  static childContextTypes = {
+    professor: PropTypes.object
+  };
+
+  getChildContext() {
+    return { professor: this.professor };
+  }
+
+  render() {
+    return Children.only(this.props.children);
+  }
+}
+
+/* eslint-disable react/display-name,  react/prop-types */
+ProfessorContext.wrap = function wrap(Component, professor) {
+  const props = {};
+  if (professor) {
+    props.professor = professor;
+  }
+
+  return React.createElement(
+    ProfessorContext,
+    props,
+    Component
+  );
+};
+
+export default ProfessorContext;
diff --git a/common/app/utils/professor-x.js b/common/app/utils/professor-x.js
new file mode 100644
index 0000000..ca1522a
--- /dev/null
+++ b/common/app/utils/professor-x.js
@@ -0,0 +1,196 @@
+import React, { PropTypes, createElement } from 'react';
+import { Observable, CompositeDisposable } from 'rx';
+import debug from 'debug';
+
+// interface contain {
+//   (options?: Object, Component: ReactComponent) => ReactComponent
+//   (options?: Object) => (Component: ReactComponent) => ReactComponent
+// }
+//
+// Action: { type: String, payload: Any, ...meta }
+//
+// ActionCreator(...args) => Observable
+//
+// interface options {
+//   fetchAction?: ActionCreator,
+//   getActionArgs?(props: Object, context: Object) => [],
+//   isPrimed?(props: Object, context: Object) => Boolean,
+//   handleError?(err) => Void
+//   shouldRefetch?(
+//     props: Object,
+//     nextProps: Object,
+//     context: Object,
+//     nextContext: Object
+//   ) => Boolean,
+// }
+
+
+const log = debug('fcc:professerx');
+
+function getChildContext(childContextTypes, currentContext) {
+
+  const compContext = { ...currentContext };
+  // istanbul ignore else
+  if (!childContextTypes || !childContextTypes.professor) {
+    delete compContext.professor;
+  }
+  return compContext;
+}
+
+const __DEV__ = process.env.NODE_ENV !== 'production';
+
+export default function contain(options = {}, Component) {
+  /* istanbul ignore else */
+  if (!Component) {
+    return contain.bind(null, options);
+  }
+
+  let action;
+  let isActionable = false;
+  let hasRefetcher = typeof options.shouldRefetch === 'function';
+
+  const getActionArgs = typeof options.getActionArgs === 'function' ?
+    options.getActionArgs :
+    (() => []);
+
+  const isPrimed = typeof typeof options.isPrimed === 'function' ?
+    options.isPrimed :
+    (() => false);
+
+
+  return class Container extends React.Component {
+    constructor(props, context) {
+      super(props, context);
+      this.__subscriptions = new CompositeDisposable();
+    }
+
+    static displayName = `Container(${Component.displayName})`;
+    static propTypes = Component.propTypes;
+
+    static contextTypes = {
+      ...Component.contextTypes,
+      professor: PropTypes.object
+    };
+
+    componentWillMount() {
+      const { professor } = this.context;
+      const { props } = this;
+      if (!options.fetchAction) {
+        log(`${Component.displayName} has no fetch action defined`);
+        return null;
+      }
+
+      action = props[options.fetchAction];
+      isActionable = typeof action === 'function';
+
+      if (__DEV__ && typeof action !== 'function') {
+        throw new Error(
+          `${options.fetchAction} should return a function but got ${action}.
+          Check the fetch options for ${Component.displayName}.`
+        );
+      }
+
+      if (
+        !professor ||
+        !professor.fetchContext
+      ) {
+        log(
+          `${Component.displayName} did not have professor defined on context`
+        );
+        return null;
+      }
+
+
+      const actionArgs = getActionArgs(
+        props,
+        getChildContext(Component.contextTypes, this.context)
+      );
+
+      professor.fetchContext.push({
+        name: options.fetchAction,
+        action,
+        actionArgs,
+        component: Component.displayName || 'Anon'
+      });
+    }
+
+    componentDidMount() {
+      if (isPrimed(this.props, this.context)) {
+        log('container is primed');
+        return null;
+      }
+      if (!isActionable) {
+        log(`${Component.displayName} container is not actionable`);
+        return null;
+      }
+      const actionArgs = getActionArgs(this.props, this.context);
+      const fetch$ = action.apply(null, actionArgs);
+      if (__DEV__ && !Observable.isObservable(fetch$)) {
+        console.log(fetch$);
+        throw new Error(
+          `Action creator should return an Observable but got ${fetch$}.
+          Check the action creator for fetch action ${options.fetchAction}`
+        );
+      }
+
+      const subscription = fetch$.subscribe(
+        () => {},
+        options.handleError
+      );
+      this.__subscriptions.add(subscription);
+    }
+
+    componentWillReceiveProps(nextProps, nextContext) {
+      if (
+        !isActionable ||
+        !hasRefetcher ||
+        !options.shouldRefetch(
+          this.props,
+          nextProps,
+          getChildContext(Component.contextTypes, this.context),
+          getChildContext(Component.contextTypes, nextContext)
+        )
+      ) {
+        return;
+      }
+      const actionArgs = getActionArgs(
+        this.props,
+        getChildContext(Component.contextTypes, this.context)
+      );
+
+      const fetch$ = action.apply(null, actionArgs);
+      if (__DEV__ && Observable.isObservable(fetch$)) {
+        throw new Error(
+          'fetch action should return observable'
+        );
+      }
+
+      const subscription = fetch$.subscribe(
+        () => {},
+        options.errorHandler
+      );
+
+      this.__subscriptions.add(subscription);
+    }
+
+    componentWillUnmount() {
+      if (this.__subscriptions) {
+        this.__subscriptions.dispose();
+      }
+    }
+
+    shouldComponentUpdate() {
+      // props should be immutable
+      return false;
+    }
+
+    render() {
+      const { props } = this;
+
+      return createElement(
+        Component,
+        props
+      );
+    }
+  };
+}
diff --git a/common/app/utils/render-to-string.js b/common/app/utils/render-to-string.js
new file mode 100644
index 0000000..b19b11b
--- /dev/null
+++ b/common/app/utils/render-to-string.js
@@ -0,0 +1,52 @@
+import { Observable, Scheduler } from 'rx';
+import ReactDOM from 'react-dom/server';
+import debug from 'debug';
+
+import ProfessorContext from './Professor-Context';
+
+const log = debug('fcc:professor');
+
+export function fetch({ fetchContext = [] }) {
+  if (fetchContext.length === 0) {
+    log('empty fetch context found');
+    return Observable.just(fetchContext);
+  }
+  return Observable.from(fetchContext, null, null, Scheduler.default)
+    .doOnNext(({ name }) => log(`calling ${name} action creator`))
+    .map(({ action, actionArgs }) => action.apply(null, actionArgs))
+    .doOnNext(fetch$ => {
+      if (!Observable.isObservable(fetch$)) {
+        throw new Error(
+          `action creator should return an observable`
+        );
+      }
+    })
+    .map(fetch$ => fetch$.doOnNext(action => log('action', action.type)))
+    .mergeAll()
+    .doOnCompleted(() => log('all fetch observables completed'));
+}
+
+
+export default function renderToString(Component) {
+  const fetchContext = [];
+  const professor = { fetchContext };
+  let ContextedComponent;
+  try {
+    ContextedComponent = ProfessorContext.wrap(Component, professor);
+    log('initiating fetcher registration');
+    ReactDOM.renderToStaticMarkup(ContextedComponent);
+    log('fetcher registration completed');
+  } catch (e) {
+    return Observable.throw(e);
+  }
+  return fetch(professor)
+    .last()
+    .delay(0)
+    .map(() => {
+      const markup = ReactDOM.renderToString(Component);
+      return {
+        markup,
+        fetchContext
+      };
+    });
+}
diff --git a/common/app/utils/render.js b/common/app/utils/render.js
new file mode 100644
index 0000000..c30c682
--- /dev/null
+++ b/common/app/utils/render.js
@@ -0,0 +1,26 @@
+import ReactDOM from 'react-dom';
+import { Disposable, Observable } from 'rx';
+import ProfessorContext from './Professor-Context';
+
+export default function render(Component, DOMContainer) {
+  let ContextedComponent;
+  try {
+    ContextedComponent = ProfessorContext.wrap(Component);
+  } catch (e) {
+    return Observable.throw(e);
+  }
+
+  return Observable.create(observer => {
+    try {
+      ReactDOM.render(ContextedComponent, DOMContainer, function() {
+        observer.onNext(this);
+      });
+    } catch (e) {
+      return observer.onError(e);
+    }
+
+    return Disposable.create(() => {
+      return ReactDOM.unmountComponentAtNode(DOMContainer);
+    });
+  });
+}
diff --git a/common/app/utils/shallow-equals.js b/common/app/utils/shallow-equals.js
new file mode 100644
index 0000000..3f8ba39
--- /dev/null
+++ b/common/app/utils/shallow-equals.js
@@ -0,0 +1,37 @@
+// original sourc
+// https://github.com/rackt/react-redux/blob/master/src/utils/shallowEqual.js
+// MIT license
+export default function shallowEqual(objA, objB) {
+  if (objA === objB) {
+    return true;
+  }
+
+  if (
+    typeof objA !== 'object' ||
+    objA === null ||
+    typeof objB !== 'object' ||
+    objB === null
+  ) {
+    return false;
+  }
+
+  var keysA = Object.keys(objA);
+  var keysB = Object.keys(objB);
+
+  if (keysA.length !== keysB.length) {
+    return false;
+  }
+
+  // Test for A's keys different from B.
+  var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
+  for (var i = 0; i < keysA.length; i++) {
+    if (
+      !bHasOwnProperty(keysA[i]) ||
+      objA[keysA[i]] !== objB[keysA[i]]
+    ) {
+      return false;
+    }
+  }
+
+  return true;
+}
diff --git a/common/models/User-Identity.js b/common/models/User-Identity.js
index a6c5241..2d1c357 100644
--- a/common/models/User-Identity.js
+++ b/common/models/User-Identity.js
@@ -10,7 +10,7 @@ import {
 
 const { defaultProfileImage } = require('../utils/constantStrings.json');
 const githubRegex = (/github/i);
-const debug = debugFactory('freecc:models:userIdent');
+const debug = debugFactory('fcc:models:userIdent');
 
 function createAccessToken(user, ttl, cb) {
   if (arguments.length === 2 && typeof ttl === 'function') {
diff --git a/common/models/promo.js b/common/models/promo.js
index 9a423bd..4dedb9a 100644
--- a/common/models/promo.js
+++ b/common/models/promo.js
@@ -1,7 +1,7 @@
 import { isAlphanumeric, isHexadecimal } from 'validator';
 import debug from 'debug';
 
-const log = debug('freecc:models:promo');
+const log = debug('fcc:models:promo');
 
 export default function promo(Promo) {
   Promo.getButton = function getButton(id, code, type = 'isNot') {
diff --git a/common/models/user.js b/common/models/user.js
index 6d45beb..952e665 100644
--- a/common/models/user.js
+++ b/common/models/user.js
@@ -7,7 +7,7 @@ import debugFactory from 'debug';
 import { saveUser, observeMethod } from '../../server/utils/rx';
 import { blacklistedUsernames } from '../../server/utils/constants';
 
-const debug = debugFactory('freecc:user:remote');
+const debug = debugFactory('fcc:user:remote');
 const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
 
 function getAboutProfile({
diff --git a/common/utils/ajax-stream.js b/common/utils/ajax-stream.js
index 665e233..1a95784 100644
--- a/common/utils/ajax-stream.js
+++ b/common/utils/ajax-stream.js
@@ -19,7 +19,7 @@
 import debugFactory from 'debug';
 import { Observable, AnonymousObservable, helpers } from 'rx';
 
-const debug = debugFactory('freecc:ajax$');
+const debug = debugFactory('fcc:ajax$');
 const root = typeof window !== 'undefined' ? window : {};
 
 // Gets the proper XMLHttpRequest for support for older IE
diff --git a/common/utils/services-creator.js b/common/utils/services-creator.js
new file mode 100644
index 0000000..aa7e8bf
--- /dev/null
+++ b/common/utils/services-creator.js
@@ -0,0 +1,48 @@
+import{ Observable, Disposable } from 'rx';
+import Fetchr from 'fetchr';
+import stampit from 'stampit';
+
+function callbackObserver(observer) {
+  return (err, res) => {
+    if (err) {
+      return observer.onError(err);
+    }
+
+    observer.onNext(res);
+    observer.onCompleted();
+  };
+}
+
+
+export default stampit({
+  init({ args: [ options ] }) {
+    this.services = new Fetchr(options);
+  },
+  methods: {
+    readService$({ service: resource, params, config }) {
+      return Observable.create(observer => {
+        this.services.read(
+          resource,
+          params,
+          config,
+          callbackObserver(observer)
+        );
+
+        return Disposable.create(() => observer.dispose());
+      });
+    },
+    createService$({ service: resource, params, body, config }) {
+      return Observable.create(function(observer) {
+        this.services.create(
+          resource,
+          params,
+          body,
+          config,
+          callbackObserver(observer)
+        );
+
+        return Disposable.create(() => observer.dispose());
+      });
+    }
+  }
+});
diff --git a/gulpfile.js b/gulpfile.js
index 470c7ad..86d1afc 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,5 +1,5 @@
 // enable debug for gulp
-process.env.DEBUG = process.env.DEBUG || 'freecc:*';
+process.env.DEBUG = process.env.DEBUG || 'fcc:*';
 
 require('babel-core/register');
 var Rx = require('rx'),
@@ -12,7 +12,7 @@ var Rx = require('rx'),
   gutil = require('gulp-util'),
   reduce = require('gulp-reduce-file'),
   sortKeys = require('sort-keys'),
-  debug = require('debug')('freecc:gulp'),
+  debug = require('debug')('fcc:gulp'),
   yargs = require('yargs'),
   concat = require('gulp-concat'),
   uglify = require('gulp-uglify'),
@@ -98,7 +98,10 @@ var paths = {
     'public/bower_components/bootstrap/dist/js/bootstrap.min.js',
     'public/bower_components/d3/d3.min.js',
     'public/bower_components/moment/min/moment.min.js',
-    'public/bower_components/moment-timezone/builds/moment-timezone-with-data.min.js',
+
+    'public/bower_components/' +
+      'moment-timezone/builds/moment-timezone-with-data.min.js',
+
     'public/bower_components/mousetrap/mousetrap.min.js',
     'public/bower_components/lightbox2/dist/js/lightbox.min.js',
     'public/bower_components/rxjs/dist/rx.all.min.js'
@@ -194,7 +197,7 @@ gulp.task('serve', ['build-manifest'], function(cb) {
     exec: path.join(__dirname, 'node_modules/.bin/babel-node'),
     env: {
       'NODE_ENV': process.env.NODE_ENV || 'development',
-      'DEBUG': process.env.DEBUG || 'freecc:*'
+      'DEBUG': process.env.DEBUG || 'fcc:*'
     }
   })
     .on('start', function() {
diff --git a/package.json b/package.json
index 8efd96a..15e4d5a 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
     "node-uuid": "^1.4.3",
     "nodemailer": "^2.1.0",
     "normalize-url": "^1.3.1",
+    "normalizr": "^2.0.0",
     "object.assign": "^4.0.3",
     "passport-facebook": "^2.0.0",
     "passport-github": "^1.0.0",
@@ -105,11 +106,18 @@
     "react-bootstrap": "~0.28.1",
     "react-dom": "~0.14.3",
     "react-motion": "~0.4.2",
+    "react-pure-render": "^1.0.2",
+    "react-redux": "^4.0.6",
     "react-router": "^1.0.0",
     "react-router-bootstrap": "https://github.com/FreeCodeCamp/react-router-bootstrap.git#freecodecamp",
     "react-toastr": "^2.4.0",
+    "react-router-redux": "^2.1.0",
     "react-vimeo": "~0.1.0",
+    "redux": "^3.0.5",
+    "redux-actions": "^0.9.1",
+    "redux-form": "^4.1.4",
     "request": "^2.65.0",
+    "reselect": "^2.0.2",
     "rev-del": "^1.0.5",
     "rx": "^4.0.0",
     "sanitize-html": "^1.11.1",
diff --git a/server/boot/a-extendUser.js b/server/boot/a-extendUser.js
index b212550..d1f58cd 100644
--- a/server/boot/a-extendUser.js
+++ b/server/boot/a-extendUser.js
@@ -1,7 +1,7 @@
 import { Observable } from 'rx';
 import debugFactory from 'debug';
 
-const debug = debugFactory('freecc:user:remote');
+const debug = debugFactory('fcc:user:remote');
 
 function destroyAllRelated(id, Model) {
   return Observable.fromNodeCallback(
diff --git a/server/boot/a-react.js b/server/boot/a-react.js
index e3d7711..009e585 100644
--- a/server/boot/a-react.js
+++ b/server/boot/a-react.js
@@ -1,14 +1,14 @@
 import React from 'react';
 import { RoutingContext } from 'react-router';
-import Fetchr from 'fetchr';
 import { createLocation } from 'history';
-import debugFactory from 'debug';
-import { dehydrate } from 'thundercats';
-import { renderToString$ } from 'thundercats-react';
+import debug from 'debug';
+
+import renderToString from '../../common/app/utils/render-to-string';
+import provideStore from '../../common/app/provide-store';
 
 import app$ from '../../common/app';
 
-const debug = debugFactory('freecc:react-server');
+const log = debug('fcc:react-server');
 
 // add routes here as they slowly get reactified
 // remove their individual controllers
@@ -38,52 +38,43 @@ export default function reactSubRouter(app) {
   app.use(router);
 
   function serveReactApp(req, res, next) {
-    const services = new Fetchr({ req });
+    const serviceOptions = { req };
     const location = createLocation(req.path);
 
     // returns a router wrapped app
-    app$({ location })
+    app$({
+      location,
+      serviceOptions
+    })
       // if react-router does not find a route send down the chain
-      .filter(function({ props }) {
+      .filter(({ props }) => {
         if (!props) {
-          debug('react tried to find %s but got 404', location.pathname);
+          log(`react tried to find ${location.pathname} but got 404`);
           return next();
         }
         return !!props;
       })
-      .flatMap(function({ props, AppCat }) {
-        const cat = AppCat(null, services);
-        debug('render react markup and pre-fetch data');
-        const store = cat.getStore('appStore');
-
-        // primes store to observe action changes
-        // cleaned up by cat.dispose further down
-        store.subscribe(() => {});
+      .flatMap(({ props, store }) => {
+        log('render react markup and pre-fetch data');
 
-        return renderToString$(
-          cat,
-          React.createElement(RoutingContext, props)
+        return renderToString(
+          provideStore(React.createElement(RoutingContext, props), store)
         )
-          .flatMap(
-            dehydrate(cat),
-            ({ markup }, data) => ({ markup, data, cat })
-          );
+          .map(({ markup }) => ({ markup, store }));
       })
-      .flatMap(function({ data, markup, cat }) {
-        debug('react markup rendered, data fetched');
-        cat.dispose();
-        const { title } = data.AppStore;
-        res.expose(data, 'data');
+      .flatMap(function({ markup, store }) {
+        log('react markup rendered, data fetched');
+        const state = store.getState();
+        const { title } = state.app.title;
+        res.expose(state, 'data');
         return res.render$(
           'layout-react',
           { markup, title }
         );
       })
+      .doOnNext(markup => res.send(markup))
       .subscribe(
-        function(markup) {
-          debug('html rendered and ready to send');
-          res.send(markup);
-        },
+        () => log('html rendered and ready to send'),
         next
       );
   }
diff --git a/server/boot/certificate.js b/server/boot/certificate.js
index e4082e5..6775edd 100644
--- a/server/boot/certificate.js
+++ b/server/boot/certificate.js
@@ -22,7 +22,7 @@ import {
 
 import certTypes from '../utils/certTypes.json';
 
-const log = debug('freecc:certification');
+const log = debug('fcc:certification');
 const sendMessageToNonUser = ifNoUserSend(
   'must be logged in to complete.'
 );
diff --git a/server/boot/challenge.js b/server/boot/challenge.js
index 957483f..1555566 100644
--- a/server/boot/challenge.js
+++ b/server/boot/challenge.js
@@ -26,7 +26,7 @@ import badIdMap from '../utils/bad-id-map';
 
 const isDev = process.env.NODE_ENV !== 'production';
 const isBeta = !!process.env.BETA;
-const log = debug('freecc:challenges');
+const log = debug('fcc:challenges');
 const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
 const challengeView = {
   0: 'challenges/showHTML',
diff --git a/server/boot/commit.js b/server/boot/commit.js
index 8d5805e..4df3af2 100644
--- a/server/boot/commit.js
+++ b/server/boot/commit.js
@@ -34,7 +34,7 @@ const sendNonUserToCommit = ifNoUserRedirectTo(
   'info'
 );
 
-const debug = debugFactory('freecc:commit');
+const debug = debugFactory('fcc:commit');
 
 function findNonprofit(name) {
   let nonprofit;
diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js
index 8daa663..ed8e886 100644
--- a/server/boot/randomAPIs.js
+++ b/server/boot/randomAPIs.js
@@ -2,7 +2,7 @@ var Rx = require('rx'),
     async = require('async'),
     moment = require('moment'),
     request = require('request'),
-    debug = require('debug')('freecc:cntr:resources'),
+    debug = require('debug')('fcc:cntr:resources'),
     constantStrings = require('../utils/constantStrings.json'),
     labs = require('../resources/labs.json'),
     testimonials = require('../resources/testimonials.json'),
diff --git a/server/boot/story.js b/server/boot/story.js
index 516ce9e..0f43aef 100755
--- a/server/boot/story.js
+++ b/server/boot/story.js
@@ -2,7 +2,7 @@ var Rx = require('rx'),
     assign = require('object.assign'),
     sanitizeHtml = require('sanitize-html'),
     moment = require('moment'),
-    debug = require('debug')('freecc:cntr:story'),
+    debug = require('debug')('fcc:cntr:story'),
     utils = require('../utils'),
     observeMethod = require('../utils/rx').observeMethod,
     saveUser = require('../utils/rx').saveUser,
diff --git a/server/boot/user.js b/server/boot/user.js
index bfb0ca6..5eebc4d 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -19,7 +19,7 @@ import {
   calcLongestStreak
 } from '../utils/user-stats';
 
-const debug = debugFactory('freecc:boot:user');
+const debug = debugFactory('fcc:boot:user');
 const sendNonUserToMap = ifNoUserRedirectTo('/map');
 const certIds = {
   [certTypes.frontEnd]: frontEndChallengeId,
diff --git a/server/services/hikes.js b/server/services/hikes.js
index bd4a681..e672d85 100644
--- a/server/services/hikes.js
+++ b/server/services/hikes.js
@@ -1,23 +1,23 @@
 import debugFactory from 'debug';
 import assign from 'object.assign';
 
-const debug = debugFactory('freecc:services:hikes');
+const debug = debugFactory('fcc:services:hikes');
 
 export default function hikesService(app) {
   const Challenge = app.models.Challenge;
 
   return {
     name: 'hikes',
-    read: (req, resource, params, config, cb) => {
+    read: (req, resource, { dashedName } = {}, config, cb) => {
       const query = {
         where: { challengeType: '6' },
         order: ['order ASC', 'suborder ASC' ]
       };
 
-      debug('params', params);
-      if (params) {
+      debug('dashedName', dashedName);
+      if (dashedName) {
         assign(query.where, {
-          dashedName: { like: params.dashedName, options: 'i' }
+          dashedName: { like: dashedName, options: 'i' }
         });
       }
       debug('query', query);
diff --git a/server/services/user.js b/server/services/user.js
index 3fa4dc4..a2816e8 100644
--- a/server/services/user.js
+++ b/server/services/user.js
@@ -2,7 +2,7 @@ import debugFactory from 'debug';
 import assign from 'object.assign';
 
 const censor = '**********************:P********';
-const debug = debugFactory('freecc:services:user');
+const debug = debugFactory('fcc:services:user');
 const protectedUserFields = {
   id: censor,
   password: censor,
diff --git a/server/utils/commit.js b/server/utils/commit.js
index 02ce7ab..0a9474c 100644
--- a/server/utils/commit.js
+++ b/server/utils/commit.js
@@ -3,7 +3,7 @@ import debugFactory from 'debug';
 import { Observable } from 'rx';
 
 import commitGoals from './commit-goals.json';
-const debug = debugFactory('freecc:utils/commit');
+const debug = debugFactory('fcc:utils/commit');
 
 export { commitGoals };
 
diff --git a/server/utils/rx.js b/server/utils/rx.js
index 68086d2..0900cc7 100644
--- a/server/utils/rx.js
+++ b/server/utils/rx.js
@@ -1,7 +1,7 @@
 import Rx from 'rx';
 import debugFactory from 'debug';
 
-const debug = debugFactory('freecc:rxUtils');
+const debug = debugFactory('fcc:rxUtils');
 
 export function saveInstance(instance) {
   return new Rx.Observable.create(function(observer) {
