Initial move to redux

This commit is contained in:
Berkeley Martinez
2016-01-27 11:34:44 -08:00
parent 2863efe8e1
commit 8ef3fdb6a0
67 changed files with 1527 additions and 667 deletions

View File

@ -232,7 +232,7 @@
"react/jsx-uses-vars": 1,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-multi-comp": 2,
"react/no-multi-comp": [2, { "ignoreStateless": true } ],
"react/prop-types": 2,
"react/react-in-jsx-scope": 1,
"react/self-closing-comp": 1,

View File

@ -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));
}

View File

@ -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(() => {});
}

View File

@ -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);
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
const shouldRouterListenForReplays = !!window.devToolsExtension;
const clientSagaOptions = { doc: document };
// 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);
// set page title
appStore$
.pluck('title')
.distinctUntilChanged()
.doOnNext(title => document.title = title)
.subscribe(() => {});
historySaga(
app$({
location: appLocation,
history,
updateLocation,
goTo,
goBack,
routerState$
);
const err$ = appStore$
.pluck('err')
.filter(err => !!err)
.distinctUntilChanged();
errSaga(err$, toast);
serviceOptions,
initialState,
middlewares: [
routingMiddleware,
...sagas.map(saga => saga(clientSagaOptions))
],
reducers: { routing },
enhancers: [ devTools ]
})
// allow store subscribe to subscribe to actions
.delay(10)
.flatMap(({ props, appCat }) => {
.flatMap(({ props, store }) => {
// 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 Normal file
View File

19
client/sagas/err-saga.js Normal file
View File

@ -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 Normal file
View File

@ -0,0 +1,4 @@
import errSaga from './err-saga';
import titleSaga from './title-saga';
export default [errSaga, titleSaga];

View File

@ -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;
};
};

View File

@ -1,50 +1,50 @@
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',
const mapStateToProps = createSelector(
state => state.app,
({
username,
points,
picture,
toast
}) => ({
username,
points,
picture,
toast
})
);
const fetchContainerOptions = {
fetchAction: 'fetchUser',
isPrimed({ username }) {
return !!username;
},
map({
username,
points,
picture,
toast
}) {
return {
username,
points,
picture,
toast
};
},
getPayload(props) {
return {
isPrimed: !!props.username
};
}
},
React.createClass({
displayName: 'FreeCodeCamp',
};
propTypes: {
appActions: PropTypes.object,
// export plain class for testing
export class FreeCodeCamp extends React.Component {
static displayName = 'FreeCodeCamp';
static propTypes = {
children: PropTypes.node,
username: PropTypes.string,
points: PropTypes.number,
picture: PropTypes.string,
toast: PropTypes.object
},
};
componentWillReceiveProps({ toast: nextToast = {} }) {
const { toast = {} } = this.props;
@ -58,15 +58,15 @@ export default contain(
}
);
}
},
}
render() {
const { username, points, picture } = this.props;
const navProps = { username, points, picture };
return (
<div>
<Nav
{ ...navProps }/>
<Nav { ...navProps }/>
<Row>
{ this.props.children }
</Row>
@ -77,5 +77,13 @@ export default contain(
</div>
);
}
})
}
const wrapComponent = compose(
// connect Component to Redux Store
connect(mapStateToProps, { fetchUser }),
// handles prefetching data
contain(fetchContainerOptions)
);
export default wrapComponent(FreeCodeCamp);

View File

@ -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 };
});
}

View File

@ -0,0 +1 @@
Currently not used

View File

@ -28,15 +28,15 @@ const toggleButtonChild = (
</Col>
);
export default React.createClass({
displayName: 'Nav',
export default class extends React.Component {
static displayName = 'Nav';
propTypes: {
static propTypes = {
points: PropTypes.number,
picture: PropTypes.string,
signedIn: PropTypes.bool,
username: PropTypes.string
},
};
renderLinks() {
return navLinks.map(({ content, link, react, target }, index) => {
@ -63,7 +63,7 @@ export default React.createClass({
</NavItem>
);
});
},
}
renderPoints(username, points) {
if (!username) {
@ -76,7 +76,7 @@ export default React.createClass({
[ { points } ]
</FCCNavItem>
);
},
}
renderSignin(username, picture) {
if (username) {
@ -100,7 +100,7 @@ export default React.createClass({
</NavItem>
);
}
},
}
render() {
const { username, points, picture } = this.props;
@ -124,4 +124,4 @@ export default React.createClass({
</Navbar>
);
}
});
}

View File

@ -6,17 +6,21 @@ function goToServer(path) {
win.location = '/' + path;
}
export default React.createClass({
displayName: 'NotFound',
propTypes: {
export default class extends React.Component {
static displayName = 'NotFound';
static propTypes = {
params: PropTypes.object
},
};
componentWillMount() {
goToServer(this.props.params.splat);
},
}
componentDidMount() {
},
}
render() {
return <span></span>;
}
});
}

79
common/app/create-app.jsx Normal file
View File

@ -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
}));
}

View File

@ -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
});
}

View File

@ -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
)
);
}
});

View File

@ -1,2 +0,0 @@
export AppActions from './Actions';
export AppStore from './Store';

View File

@ -1 +1 @@
export default from './app-stream.jsx';
export default from './create-app.jsx';

View File

View File

@ -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
);
}

View File

@ -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);

View File

@ -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);
};
};

View File

@ -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 ];

View File

@ -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 Normal file
View File

@ -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}` }), {});

View File

@ -1 +0,0 @@
future home of FAVS app

View File

@ -1,44 +1,53 @@
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',
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';
propTypes: {
currentHike: PropTypes.object,
hikesActions: PropTypes.object,
static propTypes = {
title: PropTypes.object,
params: PropTypes.object,
resetHike: PropTypes.func,
showQuestions: PropTypes.bool
},
};
componentWillUnmount() {
this.props.hikesActions.resetHike();
},
this.props.resetHike();
}
componentWillReceiveProps({ params: { dashedName } }) {
if (this.props.params.dashedName !== dashedName) {
this.props.hikesActions.resetHike();
this.props.resetHike();
}
}
},
renderBody(showQuestions) {
if (showQuestions) {
return <Questions />;
}
return <Lecture />;
},
}
render() {
const {
currentHike: { title } = {},
title,
showQuestions
} = this.props;
@ -59,5 +68,7 @@ export default contain(
</Col>
);
}
})
);
}
// export redux aware component
export default connect(mapStateToProps, { resetHike });

View File

@ -1,68 +1,72 @@
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
}),
// const log = debug('fcc:hikes');
const mapStateToProps = createSelector(
state => state.hikesApp.hikes,
hikes => {
if (!hikes || !hikes.entities || !hikes.results) {
return { hikes: [] };
}
return {
hikes: hikes.results.map(dashedName => hikes.enitites[dashedName])
};
}
);
const fetchOptions = {
fetchAction: 'fetchHikes',
isPrimed: ({ hikes }) => hikes && !!hikes.length,
getPayload: ({ params: { dashedName } }) => dashedName,
shouldContainerFetch(props, nextProps) {
return props.params.dashedName !== nextProps.params.dashedName;
}
},
React.createClass({
displayName: 'Hikes',
};
propTypes: {
appActions: PropTypes.object,
export class Hikes extends React.Component {
static displayName = 'Hikes';
static propTypes = {
children: PropTypes.element,
currentHike: PropTypes.object,
hikes: PropTypes.array,
params: PropTypes.object,
showQuestions: PropTypes.bool
},
updateTitle: PropTypes.func
};
componentWillMount() {
const { appActions } = this.props;
appActions.setTitle('Videos');
},
const { updateTitle } = this.props;
updateTitle('Hikes');
}
shouldComponentUpdate = shouldComponentUpdate;
renderMap(hikes) {
return (
<HikesMap hikes={ hikes }/>
);
},
renderChild({ children, ...props }) {
if (!children) {
return null;
}
return React.cloneElement(children, props);
},
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 }) ||
this.props.children ||
// if no sub-route render hikes map
this.renderMap(hikes)
}
@ -70,5 +74,10 @@ export default contain(
</div>
);
}
})
);
}
// export redux and fetch aware component
export default compose(
connect(mapStateToProps, { fetchHikes, updateTitle }),
contain(fetchOptions)
)(Hikes);

View File

@ -1,54 +1,51 @@
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 mapStateToProps = createSelector(
state => state.hikesApp.hikes.entities,
state => state.hikesApp.currentHike,
(hikes, currentHikeDashedName) => {
const currentHike = hikes[currentHikeDashedName];
const {
currentHike: {
dashedName,
description,
challengeSeed: [id] = [0]
} = {}
} = state.hikesApp;
} = currentHike || {};
return {
id,
dashedName,
description,
id
description
};
}
},
React.createClass({
displayName: 'Lecture',
mixins: [History],
);
propTypes: {
export class Lecture extends React.Component {
static displayName = 'Lecture';
static propTypes = {
dashedName: PropTypes.string,
description: PropTypes.array,
id: PropTypes.string,
hikesActions: PropTypes.object
},
};
shouldComponentUpdate(nextProps) {
const { props } = this;
return nextProps.id !== props.id;
},
}
handleError: debug,
handleError: log;
handleFinish(hikesActions) {
debug('loading questions');
hikesActions.toggleQuestions();
},
}
renderTranscript(transcript, dashedName) {
return transcript.map((line, index) => (
@ -58,7 +55,7 @@ export default contain(
{ line }
</p>
));
},
}
render() {
const {
@ -91,5 +88,6 @@ export default contain(
</Col>
);
}
})
);
}
export default connect(mapStateToProps, { })(Lecture);

View File

@ -1 +0,0 @@
export default from './Actions';

View File

@ -1,11 +1,6 @@
import Hikes from './components/Hikes.jsx';
import Hike from './components/Hike.jsx';
/*
* show video /hikes/someVideo
* show question /hikes/someVideo/question1
*/
export default {
path: 'videos',
component: Hikes,

View File

@ -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);

View File

@ -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
}));
};
};

View File

@ -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);
};
};

View File

@ -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 ];

View File

@ -3,7 +3,7 @@ import { Observable } from 'rx';
import { Actions } from 'thundercats';
import debugFactory from 'debug';
const debug = debugFactory('freecc:hikes:actions');
const debug = debugFactory('fcc:hikes:actions');
const noOp = { transform: () => {} };
function getCurrentHike(hikes = [{}], dashedName, currentHike) {

View File

@ -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
);

View File

@ -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}` }), {});

View File

@ -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];
}

View File

@ -26,7 +26,7 @@ import {
isURL
} from 'validator';
const debug = debugFactory('freecc:jobs:newForm');
const debug = debugFactory('fcc:jobs:newForm');
const checkValidity = [
'position',

6
common/app/sagas.js Normal file
View File

@ -0,0 +1,6 @@
import { sagas as appSagas } from './redux';
import { sagas as hikesSagas} from './routes/Hikes/redux';
export default [
...appSagas,
...hikesSagas
];

View File

@ -1,20 +1,6 @@
import { Cat } from 'thundercats';
import stamp from 'stampit';
import { Disposable, Observable } from 'rx';
import { post$, postJSON$ } from '../utils/ajax-stream.js';
import { AppActions, AppStore } from './flux';
import HikesActions from './routes/Hikes/flux';
import JobActions from './routes/Jobs/flux';
const ajaxStamp = stamp({
methods: {
postJSON$,
post$
}
});
export default Cat().init(({ instance: cat, args: [services] }) => {
const serviceStamp = stamp({
methods: {
readService$(resource, params, config) {
@ -52,14 +38,3 @@ export default Cat().init(({ instance: cat, args: [services] }) => {
}
}
});
cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services);
cat.register(AppActions.compose(serviceStamp), null, services);
cat.register(
JobActions.compose(serviceStamp, ajaxStamp),
null,
cat,
services
);
cat.register(AppStore, null, cat);
});

View File

@ -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;

View File

@ -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
);
}
};
}

View File

@ -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
};
});
}

View File

@ -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);
});
});
}

View File

@ -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;
}

View File

@ -10,7 +10,7 @@ import {
const { defaultProfileImage } = require('../utils/constantStrings.json');
const githubRegex = (/github/i);
const debug = debugFactory('freecc:models:userIdent');
const debug = debugFactory('fcc:models:userIdent');
function createAccessToken(user, ttl, cb) {
if (arguments.length === 2 && typeof ttl === 'function') {

View File

@ -1,7 +1,7 @@
import { isAlphanumeric, isHexadecimal } from 'validator';
import debug from 'debug';
const log = debug('freecc:models:promo');
const log = debug('fcc:models:promo');
export default function promo(Promo) {
Promo.getButton = function getButton(id, code, type = 'isNot') {

View File

@ -7,7 +7,7 @@ import debugFactory from 'debug';
import { saveUser, observeMethod } from '../../server/utils/rx';
import { blacklistedUsernames } from '../../server/utils/constants';
const debug = debugFactory('freecc:user:remote');
const debug = debugFactory('fcc:user:remote');
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
function getAboutProfile({

View File

@ -19,7 +19,7 @@
import debugFactory from 'debug';
import { Observable, AnonymousObservable, helpers } from 'rx';
const debug = debugFactory('freecc:ajax$');
const debug = debugFactory('fcc:ajax$');
const root = typeof window !== 'undefined' ? window : {};
// Gets the proper XMLHttpRequest for support for older IE

View File

@ -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());
});
}
}
});

View File

@ -1,5 +1,5 @@
// enable debug for gulp
process.env.DEBUG = process.env.DEBUG || 'freecc:*';
process.env.DEBUG = process.env.DEBUG || 'fcc:*';
require('babel-core/register');
var Rx = require('rx'),
@ -12,7 +12,7 @@ var Rx = require('rx'),
gutil = require('gulp-util'),
reduce = require('gulp-reduce-file'),
sortKeys = require('sort-keys'),
debug = require('debug')('freecc:gulp'),
debug = require('debug')('fcc:gulp'),
yargs = require('yargs'),
concat = require('gulp-concat'),
uglify = require('gulp-uglify'),
@ -98,7 +98,10 @@ var paths = {
'public/bower_components/bootstrap/dist/js/bootstrap.min.js',
'public/bower_components/d3/d3.min.js',
'public/bower_components/moment/min/moment.min.js',
'public/bower_components/moment-timezone/builds/moment-timezone-with-data.min.js',
'public/bower_components/' +
'moment-timezone/builds/moment-timezone-with-data.min.js',
'public/bower_components/mousetrap/mousetrap.min.js',
'public/bower_components/lightbox2/dist/js/lightbox.min.js',
'public/bower_components/rxjs/dist/rx.all.min.js'
@ -194,7 +197,7 @@ gulp.task('serve', ['build-manifest'], function(cb) {
exec: path.join(__dirname, 'node_modules/.bin/babel-node'),
env: {
'NODE_ENV': process.env.NODE_ENV || 'development',
'DEBUG': process.env.DEBUG || 'freecc:*'
'DEBUG': process.env.DEBUG || 'fcc:*'
}
})
.on('start', function() {

View File

@ -92,6 +92,7 @@
"node-uuid": "^1.4.3",
"nodemailer": "^2.1.0",
"normalize-url": "^1.3.1",
"normalizr": "^2.0.0",
"object.assign": "^4.0.3",
"passport-facebook": "^2.0.0",
"passport-github": "^1.0.0",
@ -105,11 +106,18 @@
"react-bootstrap": "~0.28.1",
"react-dom": "~0.14.3",
"react-motion": "~0.4.2",
"react-pure-render": "^1.0.2",
"react-redux": "^4.0.6",
"react-router": "^1.0.0",
"react-router-bootstrap": "https://github.com/FreeCodeCamp/react-router-bootstrap.git#freecodecamp",
"react-toastr": "^2.4.0",
"react-router-redux": "^2.1.0",
"react-vimeo": "~0.1.0",
"redux": "^3.0.5",
"redux-actions": "^0.9.1",
"redux-form": "^4.1.4",
"request": "^2.65.0",
"reselect": "^2.0.2",
"rev-del": "^1.0.5",
"rx": "^4.0.0",
"sanitize-html": "^1.11.1",

View File

@ -1,7 +1,7 @@
import { Observable } from 'rx';
import debugFactory from 'debug';
const debug = debugFactory('freecc:user:remote');
const debug = debugFactory('fcc:user:remote');
function destroyAllRelated(id, Model) {
return Observable.fromNodeCallback(

View File

@ -1,14 +1,14 @@
import React from 'react';
import { RoutingContext } from 'react-router';
import Fetchr from 'fetchr';
import { createLocation } from 'history';
import debugFactory from 'debug';
import { dehydrate } from 'thundercats';
import { renderToString$ } from 'thundercats-react';
import debug from 'debug';
import renderToString from '../../common/app/utils/render-to-string';
import provideStore from '../../common/app/provide-store';
import app$ from '../../common/app';
const debug = debugFactory('freecc:react-server');
const log = debug('fcc:react-server');
// add routes here as they slowly get reactified
// remove their individual controllers
@ -38,52 +38,43 @@ export default function reactSubRouter(app) {
app.use(router);
function serveReactApp(req, res, next) {
const services = new Fetchr({ req });
const serviceOptions = { req };
const location = createLocation(req.path);
// returns a router wrapped app
app$({ location })
app$({
location,
serviceOptions
})
// if react-router does not find a route send down the chain
.filter(function({ props }) {
.filter(({ props }) => {
if (!props) {
debug('react tried to find %s but got 404', location.pathname);
log(`react tried to find ${location.pathname} but got 404`);
return next();
}
return !!props;
})
.flatMap(function({ props, AppCat }) {
const cat = AppCat(null, services);
debug('render react markup and pre-fetch data');
const store = cat.getStore('appStore');
.flatMap(({ props, store }) => {
log('render react markup and pre-fetch data');
// primes store to observe action changes
// cleaned up by cat.dispose further down
store.subscribe(() => {});
return renderToString$(
cat,
React.createElement(RoutingContext, props)
return renderToString(
provideStore(React.createElement(RoutingContext, props), store)
)
.flatMap(
dehydrate(cat),
({ markup }, data) => ({ markup, data, cat })
);
.map(({ markup }) => ({ markup, store }));
})
.flatMap(function({ data, markup, cat }) {
debug('react markup rendered, data fetched');
cat.dispose();
const { title } = data.AppStore;
res.expose(data, 'data');
.flatMap(function({ markup, store }) {
log('react markup rendered, data fetched');
const state = store.getState();
const { title } = state.app.title;
res.expose(state, 'data');
return res.render$(
'layout-react',
{ markup, title }
);
})
.doOnNext(markup => res.send(markup))
.subscribe(
function(markup) {
debug('html rendered and ready to send');
res.send(markup);
},
() => log('html rendered and ready to send'),
next
);
}

View File

@ -22,7 +22,7 @@ import {
import certTypes from '../utils/certTypes.json';
const log = debug('freecc:certification');
const log = debug('fcc:certification');
const sendMessageToNonUser = ifNoUserSend(
'must be logged in to complete.'
);

View File

@ -26,7 +26,7 @@ import badIdMap from '../utils/bad-id-map';
const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
const log = debug('freecc:challenges');
const log = debug('fcc:challenges');
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const challengeView = {
0: 'challenges/showHTML',

View File

@ -34,7 +34,7 @@ const sendNonUserToCommit = ifNoUserRedirectTo(
'info'
);
const debug = debugFactory('freecc:commit');
const debug = debugFactory('fcc:commit');
function findNonprofit(name) {
let nonprofit;

View File

@ -2,7 +2,7 @@ var Rx = require('rx'),
async = require('async'),
moment = require('moment'),
request = require('request'),
debug = require('debug')('freecc:cntr:resources'),
debug = require('debug')('fcc:cntr:resources'),
constantStrings = require('../utils/constantStrings.json'),
labs = require('../resources/labs.json'),
testimonials = require('../resources/testimonials.json'),

View File

@ -2,7 +2,7 @@ var Rx = require('rx'),
assign = require('object.assign'),
sanitizeHtml = require('sanitize-html'),
moment = require('moment'),
debug = require('debug')('freecc:cntr:story'),
debug = require('debug')('fcc:cntr:story'),
utils = require('../utils'),
observeMethod = require('../utils/rx').observeMethod,
saveUser = require('../utils/rx').saveUser,

View File

@ -19,7 +19,7 @@ import {
calcLongestStreak
} from '../utils/user-stats';
const debug = debugFactory('freecc:boot:user');
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
const certIds = {
[certTypes.frontEnd]: frontEndChallengeId,

View File

@ -1,23 +1,23 @@
import debugFactory from 'debug';
import assign from 'object.assign';
const debug = debugFactory('freecc:services:hikes');
const debug = debugFactory('fcc:services:hikes');
export default function hikesService(app) {
const Challenge = app.models.Challenge;
return {
name: 'hikes',
read: (req, resource, params, config, cb) => {
read: (req, resource, { dashedName } = {}, config, cb) => {
const query = {
where: { challengeType: '6' },
order: ['order ASC', 'suborder ASC' ]
};
debug('params', params);
if (params) {
debug('dashedName', dashedName);
if (dashedName) {
assign(query.where, {
dashedName: { like: params.dashedName, options: 'i' }
dashedName: { like: dashedName, options: 'i' }
});
}
debug('query', query);

View File

@ -2,7 +2,7 @@ import debugFactory from 'debug';
import assign from 'object.assign';
const censor = '**********************:P********';
const debug = debugFactory('freecc:services:user');
const debug = debugFactory('fcc:services:user');
const protectedUserFields = {
id: censor,
password: censor,

View File

@ -3,7 +3,7 @@ import debugFactory from 'debug';
import { Observable } from 'rx';
import commitGoals from './commit-goals.json';
const debug = debugFactory('freecc:utils/commit');
const debug = debugFactory('fcc:utils/commit');
export { commitGoals };

View File

@ -1,7 +1,7 @@
import Rx from 'rx';
import debugFactory from 'debug';
const debug = debugFactory('freecc:rxUtils');
const debug = debugFactory('fcc:rxUtils');
export function saveInstance(instance) {
return new Rx.Observable.create(function(observer) {