future home of FAVS app
Permalink
Please sign in to comment.
Showing
with
1,511 additions
and 651 deletions.
- +1 −1 .eslintrc
- +0 −9 client/err-saga.js
- +0 −69 client/history-saga.js
- +46 −74 client/index.js
- 0 client/sagas/README.md
- +19 −0 client/sagas/err-saga.js
- +4 −0 client/sagas/index.js
- +16 −0 client/sagas/title-saga.js
- +75 −67 common/app/App.jsx
- +0 −17 common/app/app-stream.jsx
- +1 −0 common/app/components/Footer/README.md
- +11 −11 common/app/components/Nav/Nav.jsx
- +11 −7 common/app/components/NotFound/index.jsx
- +79 −0 common/app/create-app.jsx
- +12 −0 common/app/create-reducer.js
- +0 −103 common/app/flux/Store.js
- +0 −2 common/app/flux/index.js
- +1 −1 common/app/index.js
- 0 common/app/middlewares.js
- +11 −0 common/app/provide-Store.js
- +21 −0 common/app/redux/actions.js
- +39 −0 common/app/redux/fetch-user-saga.js
- +6 −0 common/app/redux/index.js
- 0 common/app/{flux/Actions.js → redux/oldActions.js}
- +38 −0 common/app/redux/reducer.js
- +14 −0 common/app/redux/types.js
- +0 −1 common/app/routes/FAVS/README.md
- +67 −56 common/app/routes/Hikes/components/Hike.jsx
- +71 −62 common/app/routes/Hikes/components/Hikes.jsx
- +80 −82 common/app/routes/Hikes/components/Lecture.jsx
- +0 −1 common/app/routes/Hikes/flux/index.js
- +0 −5 common/app/routes/Hikes/index.js
- +54 −0 common/app/routes/Hikes/redux/actions.js
- +128 −0 common/app/routes/Hikes/redux/answer-saga.js
- +46 −0 common/app/routes/Hikes/redux/fetch-hikes-saga.js
- +8 −0 common/app/routes/Hikes/redux/index.js
- +1 −1 common/app/routes/Hikes/{flux/Actions.js → redux/oldActions.js}
- +88 −0 common/app/routes/Hikes/redux/reducer.js
- +23 −0 common/app/routes/Hikes/redux/types.js
- +74 −0 common/app/routes/Hikes/redux/utils.js
- +1 −1 common/app/routes/Jobs/components/NewJob.jsx
- +6 −0 common/app/sagas.js
- +0 −25 common/app/{Cat.js → temp.js}
- +42 −0 common/app/utils/Professor-Context.js
- +196 −0 common/app/utils/professor-x.js
- +52 −0 common/app/utils/render-to-string.js
- +26 −0 common/app/utils/render.js
- +37 −0 common/app/utils/shallow-equals.js
- +1 −1 common/models/User-Identity.js
- +1 −1 common/models/promo.js
- +1 −1 common/models/user.js
- +1 −1 common/utils/ajax-stream.js
- +48 −0 common/utils/services-creator.js
- +7 −4 gulpfile.js
- +8 −0 package.json
- +1 −1 server/boot/a-extendUser.js
- +24 −33 server/boot/a-react.js
- +1 −1 server/boot/certificate.js
- +1 −1 server/boot/challenge.js
- +1 −1 server/boot/commit.js
- +1 −1 server/boot/randomAPIs.js
- +1 −1 server/boot/story.js
- +1 −1 server/boot/user.js
- +5 −5 server/services/hikes.js
- +1 −1 server/services/user.js
- +1 −1 server/utils/commit.js
- +1 −1 server/utils/rx.js
2
.eslintrc
9
client/err-saga.js
| @@ -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)); | ||
| -} |
69
client/history-saga.js
| @@ -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(() => {}); | ||
| -} |
120
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') | ||
| ); |
0
client/sagas/README.md
No changes.
19
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` | ||
| + } | ||
| + }); | ||
| + }; | ||
| +}; |
4
client/sagas/index.js
| @@ -0,0 +1,4 @@ | ||
| +import errSaga from './err-saga'; | ||
| +import titleSaga from './title-saga'; | ||
| + | ||
| +export default [errSaga, titleSaga]; |
16
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; | ||
| + }; | ||
| +}; |
142
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); |
17
common/app/app-stream.jsx
| @@ -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 }; | ||
| - }); | ||
| -} |
1
common/app/components/Footer/README.md
| @@ -0,0 +1 @@ | ||
| +Currently not used |
22
common/app/components/Nav/Nav.jsx
18
common/app/components/NotFound/index.jsx
79
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 | ||
| + })); | ||
| +} |
12
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 | ||
| + }); | ||
| +} |
103
common/app/flux/Store.js
| @@ -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 | ||
| - ) | ||
| - ); | ||
| - } | ||
| -}); |
2
common/app/flux/index.js
| @@ -1,2 +0,0 @@ | ||
| -export AppActions from './Actions'; | ||
| -export AppStore from './Store'; |
2
common/app/index.js
| @@ -1 +1 @@ | ||
| -export default from './app-stream.jsx'; | ||
| +export default from './create-app.jsx'; |
0
common/app/middlewares.js
No changes.
11
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 | ||
| + ); | ||
| +} |
21
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); |
39
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); | ||
| + }; | ||
| +}; | ||
| + |
6
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 ]; |
0
common/app/flux/Actions.js → common/app/redux/oldActions.js
File renamed without changes.
38
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 | ||
| + } | ||
| +); |
14
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}` }), {}); |
123
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 }); |
133
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); |
162
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); |
1
common/app/routes/Hikes/flux/index.js
| @@ -1 +0,0 @@ | ||
| -export default from './Actions'; |
5
common/app/routes/Hikes/index.js
54
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); |
128
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 | ||
| + })); | ||
| + }; | ||
| +}; |
46
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); | ||
| + }; | ||
| +}; |
8
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 ]; |
2
common/app/routes/Hikes/flux/Actions.js → common/app/routes/Hikes/redux/oldActions.js
88
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 | ||
| +); |
23
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}` }), {}); |
74
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]; | ||
| +} |
2
common/app/routes/Jobs/components/NewJob.jsx
6
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 | ||
| +]; |
25
common/app/Cat.js → common/app/temp.js
42
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; |
196
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 | ||
| + ); | ||
| + } | ||
| + }; | ||
| +} |
52
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 | ||
| + }; | ||
| + }); | ||
| +} |
26
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); | ||
| + }); | ||
| + }); | ||
| +} |
37
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; | ||
| +} |
2
common/models/User-Identity.js
2
common/models/promo.js
2
common/models/user.js
2
common/utils/ajax-stream.js
48
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()); | ||
| + }); | ||
| + } | ||
| + } | ||
| +}); |
11
gulpfile.js
8
package.json
2
server/boot/a-extendUser.js
57
server/boot/a-react.js
2
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
10
server/services/hikes.js
2
server/services/user.js
2
server/utils/commit.js
2
server/utils/rx.js
0 comments on commit
8ef3fdb