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 Rx from 'rx'; | ||
| import React from 'react'; | import React from 'react'; | ||
| -import Fetchr from 'fetchr'; | +import debug from 'debug'; | ||
| -import debugFactory from 'debug'; | |||
| import { Router } from 'react-router'; | import { Router } from 'react-router'; | ||
| +import { routeReducer as routing, syncHistory } from 'react-router-redux'; | |||
| import { createLocation, createHistory } from 'history'; | import { createLocation, createHistory } from 'history'; | ||
| -import { hydrate } from 'thundercats'; | |||
| -import { render$ } from 'thundercats-react'; | |||
| import app$ from '../common/app'; | import app$ from '../common/app'; | ||
| -import historySaga from './history-saga'; | +import provideStore from '../common/app/provide-store'; | ||
| -import errSaga from './err-saga'; | |||
| -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 DOMContianer = document.getElementById('fcc'); | ||
| -const catState = window.__fcc__.data || {}; | +const initialState = window.__fcc__.data; | ||
| -const services = new Fetchr({ | + | ||
| - xhrPath: '/services' | +const serviceOptions = { xhrPath: '/services' }; | ||
| -}); | |||
| Rx.config.longStackSupport = !!debug.enabled; | Rx.config.longStackSupport = !!debug.enabled; | ||
| const history = createHistory(); | const history = createHistory(); | ||
| const appLocation = createLocation( | const appLocation = createLocation( | ||
| location.pathname + location.search | location.pathname + location.search | ||
| ); | ); | ||
| +const routingMiddleware = syncHistory(history); | |||
| -// returns an observable | +const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f; | ||
| -app$({ history, location: appLocation }) | +const shouldRouterListenForReplays = !!window.devToolsExtension; | ||
| - .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); | |||
| - // set page title | +const clientSagaOptions = { doc: document }; | ||
| - appStore$ | |||
| - .pluck('title') | |||
| - .distinctUntilChanged() | |||
| - .doOnNext(title => document.title = title) | |||
| - .subscribe(() => {}); | |||
| - historySaga( | +// returns an observable | ||
| - history, | +app$({ | ||
| - updateLocation, | + location: appLocation, | ||
| - goTo, | + history, | ||
| - goBack, | + serviceOptions, | ||
| - routerState$ | + initialState, | ||
| - ); | + middlewares: [ | ||
| - | + routingMiddleware, | ||
| - const err$ = appStore$ | + ...sagas.map(saga => saga(clientSagaOptions)) | ||
| - .pluck('err') | + ], | ||
| - .filter(err => !!err) | + reducers: { routing }, | ||
| - .distinctUntilChanged(); | + enhancers: [ devTools ] | ||
| +}) | |||
| + .flatMap(({ props, store }) => { | |||
| - errSaga(err$, toast); | + // because of weirdness in react-routers match function | ||
| - }) | + // we replace the wrapped returned in props with the first one | ||
| - // allow store subscribe to subscribe to actions | + // we passed in. This might be fixed in react-router 2.0 | ||
| - .delay(10) | |||
| - .flatMap(({ props, appCat }) => { | |||
| props.history = history; | props.history = history; | ||
| - return render$( | + if (shouldRouterListenForReplays && store) { | ||
| - appCat, | + log('routing middleware listening for replays'); | ||
| - React.createElement(Router, props), | + routingMiddleware.listenForReplays(store); | ||
| + } | |||
| + | |||
| + log('rendering'); | |||
| + return render( | |||
| + provideStore(React.createElement(Router, props), store), | |||
| DOMContianer | DOMContianer | ||
| ); | ); | ||
| }) | }) | ||
| .subscribe( | .subscribe( | ||
| - () => { | + () => debug('react rendered'), | ||
| - debug('react rendered'); | + err => { throw err; }, | ||
| - }, | + () => debug('react closed subscription') | ||
| - 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 React, { PropTypes } from 'react'; | ||
| import { Row } from 'react-bootstrap'; | import { Row } from 'react-bootstrap'; | ||
| import { ToastMessage, ToastContainer } from 'react-toastr'; | 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'; | import Nav from './components/Nav'; | ||
| const toastMessageFactory = React.createFactory(ToastMessage.animation); | const toastMessageFactory = React.createFactory(ToastMessage.animation); | ||
| -export default contain( | +const mapStateToProps = createSelector( | ||
| - { | + state => state.app, | ||
| - actions: ['appActions'], | + ({ | ||
| - store: 'appStore', | + username, | ||
| - fetchAction: 'appActions.getUser', | + points, | ||
| - isPrimed({ username }) { | + picture, | ||
| - return !!username; | + toast | ||
| - }, | + }) => ({ | ||
| - map({ | + username, | ||
| - username, | + points, | ||
| - points, | + picture, | ||
| - picture, | + toast | ||
| - toast | + }) | ||
| - }) { | +); | ||
| - return { | + | ||
| - username, | +const fetchContainerOptions = { | ||
| - points, | + fetchAction: 'fetchUser', | ||
| - picture, | + isPrimed({ username }) { | ||
| - toast | + return !!username; | ||
| - }; | + } | ||
| - }, | +}; | ||
| - getPayload(props) { | |||
| - return { | |||
| - isPrimed: !!props.username | |||
| - }; | |||
| - } | |||
| - }, | |||
| - React.createClass({ | |||
| - displayName: 'FreeCodeCamp', | |||
| - propTypes: { | +// export plain class for testing | ||
| - appActions: PropTypes.object, | +export class FreeCodeCamp extends React.Component { | ||
| - children: PropTypes.node, | + static displayName = 'FreeCodeCamp'; | ||
| - username: PropTypes.string, | |||
| - points: PropTypes.number, | |||
| - picture: PropTypes.string, | |||
| - toast: PropTypes.object | |||
| - }, | |||
| - componentWillReceiveProps({ toast: nextToast = {} }) { | + static propTypes = { | ||
| - const { toast = {} } = this.props; | + children: PropTypes.node, | ||
| - if (toast.id !== nextToast.id) { | + username: PropTypes.string, | ||
| - this.refs.toaster[nextToast.type || 'success']( | + points: PropTypes.number, | ||
| - nextToast.message, | + picture: PropTypes.string, | ||
| - nextToast.title, | + toast: PropTypes.object | ||
| - { | + }; | ||
| - closeButton: true, | |||
| - timeOut: 10000 | |||
| - } | |||
| - ); | |||
| - } | |||
| - }, | |||
| - render() { | + componentWillReceiveProps({ toast: nextToast = {} }) { | ||
| - const { username, points, picture } = this.props; | + const { toast = {} } = this.props; | ||
| - const navProps = { username, points, picture }; | + if (toast.id !== nextToast.id) { | ||
| - return ( | + this.refs.toaster[nextToast.type || 'success']( | ||
| - <div> | + nextToast.message, | ||
| - <Nav | + nextToast.title, | ||
| - { ...navProps }/> | + { | ||
| - <Row> | + closeButton: true, | ||
| - { this.props.children } | + timeOut: 10000 | ||
| - </Row> | + } | ||
| - <ToastContainer | |||
| - className='toast-bottom-right' | |||
| - ref='toaster' | |||
| - toastMessageFactory={ toastMessageFactory } /> | |||
| - </div> | |||
| ); | ); | ||
| } | } | ||
| - }) | + } | ||
| + | |||
| + 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}` }), {}); | |||
1
common/app/routes/FAVS/README.md
| @@ -1 +0,0 @@ | |||
| -future home of FAVS app | |||
123
common/app/routes/Hikes/components/Hike.jsx
| @@ -1,63 +1,74 @@ | |||
| import React, { PropTypes } from 'react'; | import React, { PropTypes } from 'react'; | ||
| -import { contain } from 'thundercats-react'; | +import { connect } from 'react-redux'; | ||
| import { Col, Row } from 'react-bootstrap'; | import { Col, Row } from 'react-bootstrap'; | ||
| +import { createSelector } from 'reselect'; | |||
| import Lecture from './Lecture.jsx'; | import Lecture from './Lecture.jsx'; | ||
| import Questions from './Questions.jsx'; | import Questions from './Questions.jsx'; | ||
| +import { resetHike } from '../redux/actions'; | |||
| -export default contain( | +const mapStateToProps = createSelector( | ||
| - { | + state => state.hikesApp.hikes.entities, | ||
| - actions: ['hikesActions'] | + state => state.hikesApp.currentHike, | ||
| - }, | + (hikes, currentHikeDashedName) => { | ||
| - React.createClass({ | + const currentHike = hikes[currentHikeDashedName]; | ||
| - displayName: 'Hike', | + return { | ||
| - | + title: currentHike.title | ||
| - 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> | |||
| - ); | |||
| - } | |||
| - }) | |||
| ); | ); | ||
| +// 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 React, { PropTypes } from 'react'; | ||
| +import { compose } from 'redux'; | |||
| +import { connect } from 'react-redux'; | |||
| import { Row } from 'react-bootstrap'; | import { Row } from 'react-bootstrap'; | ||
| -import { contain } from 'thundercats-react'; | +import shouldComponentUpdate from 'react-pure-render/function'; | ||
| -// import debugFactory from 'debug'; | +import { createSelector } from 'reselect'; | ||
| +// import debug from 'debug'; | |||
| import HikesMap from './Map.jsx'; | 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( | +// const log = debug('fcc:hikes'); | ||
| - { | + | ||
| - store: 'appStore', | +const mapStateToProps = createSelector( | ||
| - map(state) { | + state => state.hikesApp.hikes, | ||
| - return state.hikesApp; | + hikes => { | ||
| - }, | + if (!hikes || !hikes.entities || !hikes.results) { | ||
| - actions: ['appActions'], | + return { hikes: [] }; | ||
| - fetchAction: 'hikesActions.fetchHikes', | |||
| - getPayload: ({ hikes, params }) => ({ | |||
| - isPrimed: (hikes && !!hikes.length), | |||
| - dashedName: params.dashedName | |||
| - }), | |||
| - shouldContainerFetch(props, nextProps) { | |||
| - return props.params.dashedName !== nextProps.params.dashedName; | |||
| } | } | ||
| - }, | + return { | ||
| - React.createClass({ | + hikes: hikes.results.map(dashedName => hikes.enitites[dashedName]) | ||
| - displayName: 'Hikes', | + }; | ||
| + } | |||
| +); | |||
| +const fetchOptions = { | |||
| + fetchAction: 'fetchHikes', | |||
| - propTypes: { | + isPrimed: ({ hikes }) => hikes && !!hikes.length, | ||
| - appActions: PropTypes.object, | + getPayload: ({ params: { dashedName } }) => dashedName, | ||
| - children: PropTypes.element, | + shouldContainerFetch(props, nextProps) { | ||
| - currentHike: PropTypes.object, | + return props.params.dashedName !== nextProps.params.dashedName; | ||
| - hikes: PropTypes.array, | + } | ||
| - params: PropTypes.object, | +}; | ||
| - showQuestions: PropTypes.bool | |||
| - }, | |||
| - componentWillMount() { | +export class Hikes extends React.Component { | ||
| - const { appActions } = this.props; | + static displayName = 'Hikes'; | ||
| - appActions.setTitle('Videos'); | |||
| - }, | |||
| - renderMap(hikes) { | + static propTypes = { | ||
| - return ( | + children: PropTypes.element, | ||
| - <HikesMap hikes={ hikes }/> | + hikes: PropTypes.array, | ||
| - ); | + params: PropTypes.object, | ||
| - }, | + updateTitle: PropTypes.func | ||
| + }; | |||
| - renderChild({ children, ...props }) { | + componentWillMount() { | ||
| - if (!children) { | + const { updateTitle } = this.props; | ||
| - return null; | + updateTitle('Hikes'); | ||
| - } | + } | ||
| - return React.cloneElement(children, props); | |||
| - }, | |||
| - render() { | + shouldComponentUpdate = shouldComponentUpdate; | ||
| - const { hikes } = this.props; | + | ||
| - const { dashedName } = this.props.params; | + renderMap(hikes) { | ||
| - const preventOverflow = { overflow: 'hidden' }; | + return ( | ||
| - return ( | + <HikesMap hikes={ hikes }/> | ||
| - <div> | + ); | ||
| - <Row style={ preventOverflow }> | + } | ||
| - { | + | ||
| - // render sub-route | + render() { | ||
| - this.renderChild({ ...this.props, dashedName }) || | + const { hikes } = this.props; | ||
| - // if no sub-route render hikes map | + const preventOverflow = { overflow: 'hidden' }; | ||
| - this.renderMap(hikes) | + return ( | ||
| - } | + <div> | ||
| - </Row> | + <Row style={ preventOverflow }> | ||
| - </div> | + { | ||
| - ); | + // 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 React, { PropTypes } from 'react'; | ||
| -import { contain } from 'thundercats-react'; | +import { connect } from 'react-redux'; | ||
| import { Button, Col, Row } from 'react-bootstrap'; | import { Button, Col, Row } from 'react-bootstrap'; | ||
| -import { History } from 'react-router'; | |||
| import Vimeo from 'react-vimeo'; | 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( | +const mapStateToProps = createSelector( | ||
| - { | + state => state.hikesApp.hikes.entities, | ||
| - actions: ['hikesActions'], | + state => state.hikesApp.currentHike, | ||
| - store: 'appStore', | + (hikes, currentHikeDashedName) => { | ||
| - map(state) { | + const currentHike = hikes[currentHikeDashedName]; | ||
| - const { | + const { | ||
| - currentHike: { | + dashedName, | ||
| - dashedName, | + description, | ||
| - description, | + challengeSeed: [id] = [0] | ||
| - challengeSeed: [id] = [0] | + } = currentHike || {}; | ||
| - } = {} | + return { | ||
| - } = state.hikesApp; | + id, | ||
| + dashedName, | |||
| + description | |||
| + }; | |||
| + } | |||
| +); | |||
| - return { | +export class Lecture extends React.Component { | ||
| - dashedName, | + static displayName = 'Lecture'; | ||
| - description, | |||
| - id | |||
| - }; | |||
| - } | |||
| - }, | |||
| - React.createClass({ | |||
| - displayName: 'Lecture', | |||
| - mixins: [History], | |||
| - propTypes: { | + static propTypes = { | ||
| - dashedName: PropTypes.string, | + dashedName: PropTypes.string, | ||
| - description: PropTypes.array, | + description: PropTypes.array, | ||
| - id: PropTypes.string, | + id: PropTypes.string, | ||
| - hikesActions: PropTypes.object | + hikesActions: PropTypes.object | ||
| - }, | + }; | ||
| - shouldComponentUpdate(nextProps) { | + shouldComponentUpdate(nextProps) { | ||
| - const { props } = this; | + const { props } = this; | ||
| - return nextProps.id !== props.id; | + return nextProps.id !== props.id; | ||
| - }, | + } | ||
| - handleError: debug, | + handleError: log; | ||
| - handleFinish(hikesActions) { | + handleFinish(hikesActions) { | ||
| - debug('loading questions'); | + debug('loading questions'); | ||
| - hikesActions.toggleQuestions(); | + hikesActions.toggleQuestions(); | ||
| - }, | + } | ||
| - renderTranscript(transcript, dashedName) { | + renderTranscript(transcript, dashedName) { | ||
| - return transcript.map((line, index) => ( | + return transcript.map((line, index) => ( | ||
| - <p | + <p | ||
| - className='lead text-left' | + className='lead text-left' | ||
| - key={ dashedName + index }> | + key={ dashedName + index }> | ||
| - { line } | + { line } | ||
| - </p> | + </p> | ||
| - )); | + )); | ||
| - }, | + } | ||
| - render() { | + render() { | ||
| - const { | + const { | ||
| - id = '1', | + id = '1', | ||
| - description = [], | + description = [], | ||
| - hikesActions | + hikesActions | ||
| - } = this.props; | + } = this.props; | ||
| - const dashedName = 'foo'; | + const dashedName = 'foo'; | ||
| - return ( | + return ( | ||
| - <Col xs={ 12 }> | + <Col xs={ 12 }> | ||
| - <Row> | + <Row> | ||
| - <Vimeo | + <Vimeo | ||
| - onError={ this.handleError } | + onError={ this.handleError } | ||
| - onFinish= { () => this.handleFinish(hikesActions) } | + onFinish= { () => this.handleFinish(hikesActions) } | ||
| - videoId={ id } /> | + videoId={ id } /> | ||
| - </Row> | + </Row> | ||
| - <Row> | + <Row> | ||
| - <article> | + <article> | ||
| - { this.renderTranscript(description, dashedName) } | + { this.renderTranscript(description, dashedName) } | ||
| - </article> | + </article> | ||
| - <Button | + <Button | ||
| - block={ true } | + block={ true } | ||
| - bsSize='large' | + bsSize='large' | ||
| - bsStyle='primary' | + bsStyle='primary' | ||
| - onClick={ () => this.handleFinish(hikesActions) }> | + onClick={ () => this.handleFinish(hikesActions) }> | ||
| - Take me to the Questions | + Take me to the Questions | ||
| - </Button> | + </Button> | ||
| - </Row> | + </Row> | ||
| - </Col> | + </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