@ -6,9 +6,10 @@ import debugFactory from 'debug';
|
|||||||
import { Router } from 'react-router';
|
import { Router } from 'react-router';
|
||||||
import { createLocation, createHistory } from 'history';
|
import { createLocation, createHistory } from 'history';
|
||||||
import { hydrate } from 'thundercats';
|
import { hydrate } from 'thundercats';
|
||||||
import { Render } from 'thundercats-react';
|
import { render$ } from 'thundercats-react';
|
||||||
|
|
||||||
import { app$ } from '../common/app';
|
import { app$ } from '../common/app';
|
||||||
|
import synchroniseHistory from './synchronise-history';
|
||||||
|
|
||||||
const debug = debugFactory('fcc:client');
|
const debug = debugFactory('fcc:client');
|
||||||
const DOMContianer = document.getElementById('fcc');
|
const DOMContianer = document.getElementById('fcc');
|
||||||
@ -23,56 +24,55 @@ const appLocation = createLocation(
|
|||||||
location.pathname + location.search
|
location.pathname + location.search
|
||||||
);
|
);
|
||||||
|
|
||||||
function location$(history) {
|
|
||||||
return Rx.Observable.create(function(observer) {
|
|
||||||
const dispose = history.listen(function(location) {
|
|
||||||
observer.onNext(location.pathname);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Rx.Disposable.create(() => {
|
|
||||||
dispose();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns an observable
|
// returns an observable
|
||||||
app$({ history, location: appLocation })
|
app$({ history, location: appLocation })
|
||||||
.flatMap(
|
.flatMap(
|
||||||
({ AppCat }) => {
|
({ AppCat }) => {
|
||||||
// instantiate the cat with service
|
// instantiate the cat with service
|
||||||
const appCat = AppCat(null, services);
|
const appCat = AppCat(null, services, history);
|
||||||
// hydrate the stores
|
// hydrate the stores
|
||||||
return hydrate(appCat, catState)
|
return hydrate(appCat, catState).map(() => appCat);
|
||||||
.map(() => appCat);
|
|
||||||
},
|
},
|
||||||
// not using nextLocation at the moment but will be used for
|
// not using nextLocation at the moment but will be used for
|
||||||
// redirects in the future
|
// redirects in the future
|
||||||
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
||||||
)
|
)
|
||||||
.doOnNext(({ appCat }) => {
|
.doOnNext(({ appCat }) => {
|
||||||
const appActions = appCat.getActions('appActions');
|
const { updateLocation, goTo, goBack } = appCat.getActions('appActions');
|
||||||
|
const appStore$ = appCat.getStore('appStore');
|
||||||
|
|
||||||
location$(history)
|
const routerState$ = appStore$
|
||||||
.pluck('pathname')
|
.map(({ location }) => location)
|
||||||
|
.distinctUntilChanged(
|
||||||
|
location => location && location.key ? location.key : location
|
||||||
|
);
|
||||||
|
|
||||||
|
// set page title
|
||||||
|
appStore$
|
||||||
|
.pluck('title')
|
||||||
|
.doOnNext(title => document.title = title)
|
||||||
|
.subscribe(() => {});
|
||||||
|
|
||||||
|
appStore$
|
||||||
|
.pluck('err')
|
||||||
|
.filter(err => !!err)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.doOnNext(route => debug('route change', route))
|
.subscribe(err => console.error(err));
|
||||||
.subscribe(route => appActions.updateRoute(route));
|
|
||||||
|
|
||||||
appActions.goBack.subscribe(function() {
|
synchroniseHistory(
|
||||||
history.goBack();
|
history,
|
||||||
});
|
updateLocation,
|
||||||
|
goTo,
|
||||||
appActions
|
goBack,
|
||||||
.updateRoute
|
routerState$
|
||||||
.pluck('route')
|
);
|
||||||
.doOnNext(route => debug('update route', route))
|
|
||||||
.subscribe(function(route) {
|
|
||||||
history.pushState(null, route);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
// allow store subscribe to subscribe to actions
|
||||||
|
.delay(10)
|
||||||
.flatMap(({ props, appCat }) => {
|
.flatMap(({ props, appCat }) => {
|
||||||
props.history = history;
|
props.history = history;
|
||||||
return Render(
|
|
||||||
|
return render$(
|
||||||
appCat,
|
appCat,
|
||||||
React.createElement(Router, props),
|
React.createElement(Router, props),
|
||||||
DOMContianer
|
DOMContianer
|
||||||
|
69
client/synchronise-history.js
Normal file
69
client/synchronise-history.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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 synchroniseHistory(
|
||||||
|
history,
|
||||||
|
updateLocation,
|
||||||
|
goTo,
|
||||||
|
goBack,
|
||||||
|
routerState$
|
||||||
|
) {
|
||||||
|
routerState$.subscribe(
|
||||||
|
location => {
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// store location has changed, update history
|
||||||
|
if (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(() => {});
|
||||||
|
}
|
@ -8,6 +8,9 @@ export default contain(
|
|||||||
{
|
{
|
||||||
store: 'appStore',
|
store: 'appStore',
|
||||||
fetchAction: 'appActions.getUser',
|
fetchAction: 'appActions.getUser',
|
||||||
|
isPrimed({ username }) {
|
||||||
|
return !!username;
|
||||||
|
},
|
||||||
getPayload(props) {
|
getPayload(props) {
|
||||||
return {
|
return {
|
||||||
isPrimed: !!props.username
|
isPrimed: !!props.username
|
||||||
@ -25,22 +28,6 @@ export default contain(
|
|||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const title = this.props.title;
|
|
||||||
this.setTitle(title);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (nextProps.title !== this.props.title) {
|
|
||||||
this.setTitle(nextProps.title);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setTitle(title) {
|
|
||||||
const doc = typeof document !== 'undefined' ? document : {};
|
|
||||||
doc.title = title;
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { username, points, picture } = this.props;
|
const { username, points, picture } = this.props;
|
||||||
const navProps = { username, points, picture };
|
const navProps = { username, points, picture };
|
||||||
|
@ -1,17 +1,65 @@
|
|||||||
import { Cat } from 'thundercats';
|
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 { AppActions, AppStore } from './flux';
|
||||||
import { HikesActions, HikesStore } from './routes/Hikes/flux';
|
import { HikesActions } from './routes/Hikes/flux';
|
||||||
import { JobActions, JobsStore} from './routes/Jobs/flux';
|
import { JobActions } from './routes/Jobs/flux';
|
||||||
|
|
||||||
export default Cat()
|
const ajaxStamp = stamp({
|
||||||
.init(({ instance: cat, args: [services] }) => {
|
methods: {
|
||||||
cat.register(AppActions, null, services);
|
postJSON$,
|
||||||
cat.register(AppStore, null, cat);
|
post$
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cat.register(HikesActions, null, services);
|
export default Cat().init(({ instance: cat, args: [services] }) => {
|
||||||
cat.register(HikesStore, null, cat);
|
const serviceStamp = stamp({
|
||||||
|
methods: {
|
||||||
|
readService$(resource, params, config) {
|
||||||
|
|
||||||
cat.register(JobActions, null, cat, services);
|
return Observable.create(function(observer) {
|
||||||
cat.register(JobsStore, null, cat);
|
services.read(resource, params, config, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return observer.onError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.onNext(res);
|
||||||
|
observer.onCompleted();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Disposable.create(function() {
|
||||||
|
observer.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createService$(resource, params, body, config) {
|
||||||
|
return Observable.create(function(observer) {
|
||||||
|
services.create(resource, params, body, config, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return observer.onError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.onNext(res);
|
||||||
|
observer.onCompleted();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Disposable.create(function() {
|
||||||
|
observer.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services);
|
||||||
|
cat.register(AppActions.compose(serviceStamp), null, services);
|
||||||
|
cat.register(
|
||||||
|
JobActions.compose(serviceStamp, ajaxStamp),
|
||||||
|
null,
|
||||||
|
cat,
|
||||||
|
services
|
||||||
|
);
|
||||||
|
cat.register(AppStore, null, cat);
|
||||||
|
});
|
||||||
|
@ -1,49 +1,45 @@
|
|||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
import debugFactory from 'debug';
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:app:actions');
|
|
||||||
|
|
||||||
export default Actions({
|
export default Actions({
|
||||||
|
shouldBindMethods: true,
|
||||||
|
refs: { displayName: 'AppActions' },
|
||||||
|
|
||||||
setTitle(title = 'Learn To Code') {
|
setTitle(title = 'Learn To Code') {
|
||||||
return { title: title + '| Free Code Camp' };
|
return { title: title + ' | Free Code Camp' };
|
||||||
},
|
},
|
||||||
|
|
||||||
setUser({
|
getUser() {
|
||||||
|
return this.readService$('user', null, null)
|
||||||
|
.map(({
|
||||||
username,
|
username,
|
||||||
picture,
|
picture,
|
||||||
progressTimestamps = [],
|
progressTimestamps = [],
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
isFullStackCert
|
isFullStackCert
|
||||||
}) {
|
}) => {
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
picture,
|
picture,
|
||||||
points: progressTimestamps.length,
|
points: progressTimestamps.length,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
isFullStackCert
|
isFullStackCert
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.catch(err => Observable.just({ err }));
|
||||||
},
|
},
|
||||||
|
|
||||||
getUser: null,
|
// routing
|
||||||
updateRoute(route) {
|
goTo: null,
|
||||||
return { route };
|
goBack: null,
|
||||||
},
|
updateLocation(location) {
|
||||||
goBack: null
|
return {
|
||||||
})
|
transform(state) {
|
||||||
.refs({ displayName: 'AppActions' })
|
return { ...state, location };
|
||||||
.init(({ instance: appActions, args: [services] }) => {
|
|
||||||
appActions.getUser.subscribe(({ isPrimed }) => {
|
|
||||||
if (isPrimed) {
|
|
||||||
debug('isPrimed');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
services.read('user', null, null, (err, user) => {
|
};
|
||||||
if (err) {
|
|
||||||
return debug('user service error');
|
|
||||||
}
|
}
|
||||||
debug('user service returned successful');
|
});
|
||||||
return appActions.setUser(user);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return appActions;
|
|
||||||
});
|
|
||||||
|
@ -5,7 +5,16 @@ const initValue = {
|
|||||||
title: 'Learn To Code | Free Code Camp',
|
title: 'Learn To Code | Free Code Camp',
|
||||||
username: null,
|
username: null,
|
||||||
picture: null,
|
picture: null,
|
||||||
points: 0
|
points: 0,
|
||||||
|
hikesApp: {
|
||||||
|
hikes: [],
|
||||||
|
// lecture state
|
||||||
|
currentHike: {},
|
||||||
|
showQuestions: false
|
||||||
|
},
|
||||||
|
jobsApp: {
|
||||||
|
showModal: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Store({
|
export default Store({
|
||||||
@ -13,12 +22,82 @@ export default Store({
|
|||||||
displayName: 'AppStore',
|
displayName: 'AppStore',
|
||||||
value: initValue
|
value: initValue
|
||||||
},
|
},
|
||||||
init({ instance: appStore, args: [cat] }) {
|
init({ instance: store, args: [cat] }) {
|
||||||
const { updateRoute, setUser, setTitle } = cat.getActions('appActions');
|
const register = createRegistrar(store);
|
||||||
const register = createRegistrar(appStore);
|
// app
|
||||||
|
const {
|
||||||
|
updateLocation,
|
||||||
|
getUser,
|
||||||
|
setTitle
|
||||||
|
} = cat.getActions('appActions');
|
||||||
|
|
||||||
register(setter(fromMany(setUser, setTitle, updateRoute)));
|
register(
|
||||||
|
fromMany(
|
||||||
|
setter(
|
||||||
|
fromMany(
|
||||||
|
getUser,
|
||||||
|
setTitle
|
||||||
|
)
|
||||||
|
),
|
||||||
|
updateLocation
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return appStore;
|
// hikes
|
||||||
|
const {
|
||||||
|
toggleQuestions,
|
||||||
|
fetchHikes,
|
||||||
|
hideInfo,
|
||||||
|
resetHike,
|
||||||
|
grabQuestion,
|
||||||
|
releaseQuestion,
|
||||||
|
moveQuestion,
|
||||||
|
answer
|
||||||
|
} = cat.getActions('hikesActions');
|
||||||
|
|
||||||
|
register(
|
||||||
|
fromMany(
|
||||||
|
toggleQuestions,
|
||||||
|
fetchHikes,
|
||||||
|
hideInfo,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
65
common/app/routes/Hikes/components/Hike.jsx
Normal file
65
common/app/routes/Hikes/components/Hike.jsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { contain } from 'thundercats-react';
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
Panel,
|
||||||
|
Row
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import Lecture from './Lecture.jsx';
|
||||||
|
import Questions from './Questions.jsx';
|
||||||
|
|
||||||
|
export default contain(
|
||||||
|
{
|
||||||
|
actions: ['hikesActions']
|
||||||
|
},
|
||||||
|
React.createClass({
|
||||||
|
displayName: 'Hike',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
currentHike: PropTypes.object,
|
||||||
|
hikesActions: PropTypes.object,
|
||||||
|
params: PropTypes.object,
|
||||||
|
showQuestions: PropTypes.bool
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.hikesActions.resetHike();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps({ params: { dashedName } }) {
|
||||||
|
if (this.props.params.dashedName !== dashedName) {
|
||||||
|
this.props.hikesActions.resetHike();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderBody(showQuestions) {
|
||||||
|
if (showQuestions) {
|
||||||
|
return <Questions />;
|
||||||
|
}
|
||||||
|
return <Lecture />;
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currentHike: { title } = {},
|
||||||
|
showQuestions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const videoTitle = <h4>{ title }</h4>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={ 12 }>
|
||||||
|
<Row>
|
||||||
|
<Panel
|
||||||
|
className={ 'text-center' }
|
||||||
|
header={ videoTitle }
|
||||||
|
title={ title }>
|
||||||
|
{ this.renderBody(showQuestions) }
|
||||||
|
</Panel>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
@ -9,7 +9,10 @@ import HikesMap from './Map.jsx';
|
|||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'hikesStore',
|
store: 'appStore',
|
||||||
|
map(state) {
|
||||||
|
return state.hikesApp;
|
||||||
|
},
|
||||||
actions: ['appActions'],
|
actions: ['appActions'],
|
||||||
fetchAction: 'hikesActions.fetchHikes',
|
fetchAction: 'hikesActions.fetchHikes',
|
||||||
getPayload: ({ hikes, params }) => ({
|
getPayload: ({ hikes, params }) => ({
|
||||||
@ -27,7 +30,9 @@ export default contain(
|
|||||||
appActions: PropTypes.object,
|
appActions: PropTypes.object,
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
currentHike: PropTypes.object,
|
currentHike: PropTypes.object,
|
||||||
hikes: PropTypes.array
|
hikes: PropTypes.array,
|
||||||
|
params: PropTypes.object,
|
||||||
|
showQuestions: PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
@ -41,21 +46,26 @@ export default contain(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChild(children, hikes, currentHike) {
|
renderChild({ children, ...props }) {
|
||||||
if (!children) {
|
if (!children) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return React.cloneElement(children, { hikes, currentHike });
|
return React.cloneElement(children, props);
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hikes, children, currentHike } = this.props;
|
const { hikes } = this.props;
|
||||||
|
const { dashedName } = this.props.params;
|
||||||
const preventOverflow = { overflow: 'hidden' };
|
const preventOverflow = { overflow: 'hidden' };
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row style={ preventOverflow }>
|
<Row style={ preventOverflow }>
|
||||||
{ this.renderChild(children, hikes, currentHike) ||
|
{
|
||||||
this.renderMap(hikes) }
|
// render sub-route
|
||||||
|
this.renderChild({ ...this.props, dashedName }) ||
|
||||||
|
// if no sub-route render hikes map
|
||||||
|
this.renderMap(hikes)
|
||||||
|
}
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,26 +1,53 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Button, Col, Row, Panel } from 'react-bootstrap';
|
import { contain } from 'thundercats-react';
|
||||||
|
import { Button, Col, Row } from 'react-bootstrap';
|
||||||
import { History } from 'react-router';
|
import { History } from 'react-router';
|
||||||
import Vimeo from 'react-vimeo';
|
import Vimeo from 'react-vimeo';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:hikes');
|
const debug = debugFactory('freecc:hikes');
|
||||||
|
|
||||||
export default React.createClass({
|
export default contain(
|
||||||
|
{
|
||||||
|
actions: ['hikesActions'],
|
||||||
|
store: 'appStore',
|
||||||
|
map(state) {
|
||||||
|
const {
|
||||||
|
currentHike: {
|
||||||
|
dashedName,
|
||||||
|
description,
|
||||||
|
challengeSeed: [id] = [0]
|
||||||
|
} = {}
|
||||||
|
} = state.hikesApp;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dashedName,
|
||||||
|
description,
|
||||||
|
id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createClass({
|
||||||
displayName: 'Lecture',
|
displayName: 'Lecture',
|
||||||
mixins: [History],
|
mixins: [History],
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
currentHike: PropTypes.object,
|
dashedName: PropTypes.string,
|
||||||
params: PropTypes.object
|
description: PropTypes.array,
|
||||||
|
id: PropTypes.string,
|
||||||
|
hikesActions: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
const { props } = this;
|
||||||
|
return nextProps.id !== props.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleError: debug,
|
handleError: debug,
|
||||||
|
|
||||||
handleFinish() {
|
handleFinish(hikesActions) {
|
||||||
debug('loading questions');
|
debug('loading questions');
|
||||||
const { dashedName } = this.props.params;
|
hikesActions.toggleQuestions();
|
||||||
this.history.pushState(null, `/hikes/${dashedName}/questions/1`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderTranscript(transcript, dashedName) {
|
renderTranscript(transcript, dashedName) {
|
||||||
@ -31,41 +58,31 @@ export default React.createClass({
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
id = '1',
|
||||||
challengeSeed = ['1'],
|
description = [],
|
||||||
description = []
|
hikesActions
|
||||||
} = this.props.currentHike;
|
} = this.props;
|
||||||
const { dashedName } = this.props.params;
|
const dashedName = 'foo';
|
||||||
|
|
||||||
const [ id ] = challengeSeed;
|
|
||||||
|
|
||||||
const videoTitle = <h2>{ title }</h2>;
|
|
||||||
return (
|
return (
|
||||||
<Col xs={ 12 }>
|
<Col xs={ 12 }>
|
||||||
<Row>
|
<Row>
|
||||||
<Panel className={ 'text-center' } title={ videoTitle }>
|
|
||||||
<Vimeo
|
<Vimeo
|
||||||
onError={ this.handleError }
|
onError={ this.handleError }
|
||||||
onFinish= { this.handleFinish }
|
onFinish= { () => this.handleFinish(hikesActions) }
|
||||||
videoId={ id } />
|
videoId={ id } />
|
||||||
</Panel>
|
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Panel>
|
|
||||||
{ this.renderTranscript(description, dashedName) }
|
{ this.renderTranscript(description, dashedName) }
|
||||||
</Panel>
|
|
||||||
<Panel>
|
|
||||||
<Button
|
<Button
|
||||||
block={ true }
|
block={ true }
|
||||||
bsSize='large'
|
bsSize='large'
|
||||||
onClick={ this.handleFinish }>
|
onClick={ () => this.handleFinish(hikesActions) }>
|
||||||
Take me to the Questions
|
Take me to the Questions
|
||||||
</Button>
|
</Button>
|
||||||
</Panel>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
@ -11,7 +11,7 @@ export default React.createClass({
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
hikes
|
hikes = [{}]
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const vidElements = hikes.map(({ title, dashedName}) => {
|
const vidElements = hikes.map(({ title, dashedName}) => {
|
||||||
|
@ -1,278 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { Motion } from 'react-motion';
|
|
||||||
import { History, Lifecycle } from 'react-router';
|
|
||||||
import debugFactory from 'debug';
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Col,
|
|
||||||
Modal,
|
|
||||||
Panel,
|
|
||||||
Row
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
|
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream.js';
|
|
||||||
|
|
||||||
const debug = debugFactory('freecc:hikes');
|
|
||||||
const ANSWER_THRESHOLD = 200;
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
displayName: 'Question',
|
|
||||||
|
|
||||||
mixins: [
|
|
||||||
History,
|
|
||||||
Lifecycle
|
|
||||||
],
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
currentHike: PropTypes.object,
|
|
||||||
dashedName: PropTypes.string,
|
|
||||||
hikes: PropTypes.array,
|
|
||||||
params: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: () => ({
|
|
||||||
mouse: [0, 0],
|
|
||||||
correct: false,
|
|
||||||
delta: [0, 0],
|
|
||||||
isPressed: false,
|
|
||||||
showInfo: false,
|
|
||||||
shake: false
|
|
||||||
}),
|
|
||||||
|
|
||||||
getTweenValues() {
|
|
||||||
const { mouse: [x, y] } = this.state;
|
|
||||||
return {
|
|
||||||
val: { x, y },
|
|
||||||
config: [120, 10]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMouseDown({ pageX, pageY, touches }) {
|
|
||||||
if (touches) {
|
|
||||||
({ pageX, pageY } = touches[0]);
|
|
||||||
}
|
|
||||||
const { mouse: [pressX, pressY] } = this.state;
|
|
||||||
const dx = pageX - pressX;
|
|
||||||
const dy = pageY - pressY;
|
|
||||||
this.setState({
|
|
||||||
isPressed: true,
|
|
||||||
delta: [dx, dy],
|
|
||||||
mouse: [pageX - dx, pageY - dy]
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMouseUp() {
|
|
||||||
const { correct } = this.state;
|
|
||||||
if (correct) {
|
|
||||||
return this.setState({
|
|
||||||
isPressed: false,
|
|
||||||
delta: [0, 0]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
isPressed: false,
|
|
||||||
mouse: [0, 0],
|
|
||||||
delta: [0, 0]
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMouseMove(answer) {
|
|
||||||
return (e) => {
|
|
||||||
let { pageX, pageY, touches } = e;
|
|
||||||
|
|
||||||
if (touches) {
|
|
||||||
e.preventDefault();
|
|
||||||
// these reassins the values of pageX, pageY from touches
|
|
||||||
({ pageX, pageY } = touches[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isPressed, delta: [dx, dy] } = this.state;
|
|
||||||
if (isPressed) {
|
|
||||||
const mouse = [pageX - dx, pageY - dy];
|
|
||||||
if (mouse[0] >= ANSWER_THRESHOLD) {
|
|
||||||
this.handleMouseUp();
|
|
||||||
return this.onAnswer(answer, true)();
|
|
||||||
}
|
|
||||||
if (mouse[0] <= -ANSWER_THRESHOLD) {
|
|
||||||
this.handleMouseUp();
|
|
||||||
return this.onAnswer(answer, false)();
|
|
||||||
}
|
|
||||||
this.setState({ mouse });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
hideInfo() {
|
|
||||||
this.setState({ showInfo: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
onAnswer(answer, userAnswer) {
|
|
||||||
return (e) => {
|
|
||||||
if (e && e.preventDefault) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.disposeTimeout) {
|
|
||||||
clearTimeout(this.disposeTimeout);
|
|
||||||
this.disposeTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (answer === userAnswer) {
|
|
||||||
debug('correct answer!');
|
|
||||||
this.setState({
|
|
||||||
correct: true,
|
|
||||||
mouse: [ userAnswer ? 1000 : -1000, 0]
|
|
||||||
});
|
|
||||||
this.disposeTimeout = setTimeout(() => {
|
|
||||||
this.onCorrectAnswer();
|
|
||||||
}, 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('incorrect');
|
|
||||||
this.setState({
|
|
||||||
showInfo: true,
|
|
||||||
shake: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.disposeTimeout = setTimeout(
|
|
||||||
() => this.setState({ shake: false }),
|
|
||||||
500
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onCorrectAnswer() {
|
|
||||||
const { hikes, currentHike } = this.props;
|
|
||||||
const { dashedName, number } = this.props.params;
|
|
||||||
const { id, name, difficulty, tests } = currentHike;
|
|
||||||
const nextQuestionIndex = +number;
|
|
||||||
|
|
||||||
postJSON$('/completed-challenge', { id, name }).subscribeOnCompleted(() => {
|
|
||||||
if (tests[nextQuestionIndex]) {
|
|
||||||
return this.history.pushState(
|
|
||||||
null,
|
|
||||||
`/hikes/${ dashedName }/questions/${ nextQuestionIndex + 1 }`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// next questions does not exist;
|
|
||||||
debug('finding next hike');
|
|
||||||
const nextHike = [].slice.call(hikes)
|
|
||||||
// hikes is in oder of difficulty, lets get reverse order
|
|
||||||
.reverse()
|
|
||||||
// now lets find the hike with the difficulty right above this one
|
|
||||||
.reduce((lowerHike, hike) => {
|
|
||||||
if (hike.difficulty > difficulty) {
|
|
||||||
return hike;
|
|
||||||
}
|
|
||||||
return lowerHike;
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
if (nextHike) {
|
|
||||||
return this.history.pushState(null, `/hikes/${ nextHike.dashedName }`);
|
|
||||||
}
|
|
||||||
debug(
|
|
||||||
'next Hike was not found, currentHike %s',
|
|
||||||
currentHike.dashedName
|
|
||||||
);
|
|
||||||
this.history.pushState(null, '/hikes');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
routerWillLeave(nextState, router, cb) {
|
|
||||||
// TODO(berks): do animated transitions here stuff here
|
|
||||||
this.setState({
|
|
||||||
showInfo: false,
|
|
||||||
correct: false,
|
|
||||||
mouse: [0, 0]
|
|
||||||
}, cb);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderInfo(showInfo, info) {
|
|
||||||
if (!info) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
backdrop={ true }
|
|
||||||
onHide={ this.hideInfo }
|
|
||||||
show={ showInfo }>
|
|
||||||
<Modal.Body>
|
|
||||||
<h3>
|
|
||||||
{ info }
|
|
||||||
</h3>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='large'
|
|
||||||
onClick={ this.hideInfo }>
|
|
||||||
hide
|
|
||||||
</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderQuestion(number, question, answer, shake) {
|
|
||||||
return ({ x: xFunc }) => {
|
|
||||||
const x = xFunc().val.x;
|
|
||||||
const style = {
|
|
||||||
WebkitTransform: `translate3d(${ x }px, 0, 0)`,
|
|
||||||
transform: `translate3d(${ x }px, 0, 0)`
|
|
||||||
};
|
|
||||||
const title = <h4>Question { number }</h4>;
|
|
||||||
return (
|
|
||||||
<Panel
|
|
||||||
className={ shake ? 'animated swing shake' : '' }
|
|
||||||
header={ title }
|
|
||||||
onMouseDown={ this.handleMouseDown }
|
|
||||||
onMouseLeave={ this.handleMouseUp }
|
|
||||||
onMouseMove={ this.handleMouseMove(answer) }
|
|
||||||
onMouseUp={ this.handleMouseUp }
|
|
||||||
onTouchEnd={ this.handleMouseUp }
|
|
||||||
onTouchMove={ this.handleMouseMove(answer) }
|
|
||||||
onTouchStart={ this.handleMouseDown }
|
|
||||||
style={ style }>
|
|
||||||
<p>{ question }</p>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { showInfo, shake } = this.state;
|
|
||||||
const { currentHike: { tests = [] } } = this.props;
|
|
||||||
const { number = '1' } = this.props.params;
|
|
||||||
|
|
||||||
const [question, answer, info] = tests[number - 1] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col
|
|
||||||
onMouseUp={ this.handleMouseUp }
|
|
||||||
xs={ 8 }
|
|
||||||
xsOffset={ 2 }>
|
|
||||||
<Row>
|
|
||||||
<Motion style={{ x: this.getTweenValues }}>
|
|
||||||
{ this.renderQuestion(number, question, answer, shake) }
|
|
||||||
</Motion>
|
|
||||||
{ this.renderInfo(showInfo, info) }
|
|
||||||
<Panel>
|
|
||||||
<Button
|
|
||||||
bsSize='large'
|
|
||||||
className='pull-left'
|
|
||||||
onClick={ this.onAnswer(answer, false, info) }>
|
|
||||||
false
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
bsSize='large'
|
|
||||||
className='pull-right'
|
|
||||||
onClick={ this.onAnswer(answer, true, info) }>
|
|
||||||
true
|
|
||||||
</Button>
|
|
||||||
</Panel>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
217
common/app/routes/Hikes/components/Questions.jsx
Normal file
217
common/app/routes/Hikes/components/Questions.jsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { spring, Motion } from 'react-motion';
|
||||||
|
import { contain } from 'thundercats-react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Panel,
|
||||||
|
Row
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
const ANSWER_THRESHOLD = 200;
|
||||||
|
|
||||||
|
export default contain(
|
||||||
|
{
|
||||||
|
store: 'appStore',
|
||||||
|
actions: ['hikesActions'],
|
||||||
|
map({ hikesApp, username }) {
|
||||||
|
const {
|
||||||
|
currentHike,
|
||||||
|
currentQuestion = 1,
|
||||||
|
mouse = [0, 0],
|
||||||
|
isCorrect = false,
|
||||||
|
delta = [0, 0],
|
||||||
|
isPressed = false,
|
||||||
|
showInfo = false,
|
||||||
|
shake = false
|
||||||
|
} = hikesApp;
|
||||||
|
return {
|
||||||
|
hike: currentHike,
|
||||||
|
currentQuestion,
|
||||||
|
mouse,
|
||||||
|
isCorrect,
|
||||||
|
delta,
|
||||||
|
isPressed,
|
||||||
|
showInfo,
|
||||||
|
shake,
|
||||||
|
username
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createClass({
|
||||||
|
displayName: 'Questions',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
hike: PropTypes.object,
|
||||||
|
currentQuestion: PropTypes.number,
|
||||||
|
mouse: PropTypes.array,
|
||||||
|
isCorrect: PropTypes.bool,
|
||||||
|
delta: PropTypes.array,
|
||||||
|
isPressed: PropTypes.bool,
|
||||||
|
showInfo: PropTypes.bool,
|
||||||
|
shake: PropTypes.bool,
|
||||||
|
username: PropTypes.string,
|
||||||
|
hikesActions: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMouseDown({ pageX, pageY, touches }) {
|
||||||
|
if (touches) {
|
||||||
|
({ pageX, pageY } = touches[0]);
|
||||||
|
}
|
||||||
|
const { mouse: [pressX, pressY], hikesActions } = this.props;
|
||||||
|
hikesActions.grabQuestion({ pressX, pressY, pageX, pageY });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMouseUp() {
|
||||||
|
if (!this.props.isPressed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.props.hikesActions.releaseQuestion();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMouseMove(answer) {
|
||||||
|
if (!this.props.isPressed) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (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: [dx, dy], hikesActions } = this.props;
|
||||||
|
const mouse = [pageX - dx, pageY - dy];
|
||||||
|
|
||||||
|
if (mouse[0] >= ANSWER_THRESHOLD) {
|
||||||
|
return this.onAnswer(answer, true)();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse[0] <= -ANSWER_THRESHOLD) {
|
||||||
|
return this.onAnswer(answer, false)();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hikesActions.moveQuestion(mouse);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onAnswer(answer, userAnswer) {
|
||||||
|
const { hikesActions } = this.props;
|
||||||
|
return (e) => {
|
||||||
|
if (e && e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hikesActions.answer({
|
||||||
|
answer,
|
||||||
|
userAnswer,
|
||||||
|
props: this.props
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
routerWillLeave(nextState, router, cb) {
|
||||||
|
// TODO(berks): do animated transitions here stuff here
|
||||||
|
this.setState({
|
||||||
|
showInfo: false,
|
||||||
|
isCorrect: false,
|
||||||
|
mouse: [0, 0]
|
||||||
|
}, cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderInfo(showInfo, info, hideInfo) {
|
||||||
|
if (!info) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
backdrop={ true }
|
||||||
|
onHide={ hideInfo }
|
||||||
|
show={ showInfo }>
|
||||||
|
<Modal.Body>
|
||||||
|
<h3>
|
||||||
|
{ info }
|
||||||
|
</h3>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
onClick={ hideInfo }>
|
||||||
|
hide
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderQuestion(number, question, answer, shake) {
|
||||||
|
return ({ x }) => {
|
||||||
|
const style = {
|
||||||
|
WebkitTransform: `translate3d(${ x }px, 0, 0)`,
|
||||||
|
transform: `translate3d(${ x }px, 0, 0)`
|
||||||
|
};
|
||||||
|
const title = <h4>Question { number }</h4>;
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
className={ shake ? 'animated swing shake' : '' }
|
||||||
|
header={ title }
|
||||||
|
onMouseDown={ this.handleMouseDown }
|
||||||
|
onMouseLeave={ this.handleMouseUp }
|
||||||
|
onMouseMove={ this.handleMouseMove(answer) }
|
||||||
|
onMouseUp={ this.handleMouseUp }
|
||||||
|
onTouchEnd={ this.handleMouseUp }
|
||||||
|
onTouchMove={ this.handleMouseMove(answer) }
|
||||||
|
onTouchStart={ this.handleMouseDown }
|
||||||
|
style={ style }>
|
||||||
|
<p>{ question }</p>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { showInfo, shake } = this.props;
|
||||||
|
const {
|
||||||
|
hike: { tests = [] } = {},
|
||||||
|
mouse: [x],
|
||||||
|
currentQuestion,
|
||||||
|
hikesActions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const [ question, answer, info ] = tests[currentQuestion - 1] || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
onMouseUp={ this.handleMouseUp }
|
||||||
|
xs={ 8 }
|
||||||
|
xsOffset={ 2 }>
|
||||||
|
<Row>
|
||||||
|
<Motion style={{ x: spring(x, [120, 10]) }}>
|
||||||
|
{ this.renderQuestion(currentQuestion, question, answer, shake) }
|
||||||
|
</Motion>
|
||||||
|
{ this.renderInfo(showInfo, info, hikesActions.hideInfo) }
|
||||||
|
<Panel>
|
||||||
|
<Button
|
||||||
|
bsSize='large'
|
||||||
|
className='pull-left'
|
||||||
|
onClick={ this.onAnswer(answer, false) }>
|
||||||
|
false
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bsSize='large'
|
||||||
|
className='pull-right'
|
||||||
|
onClick={ this.onAnswer(answer, true) }>
|
||||||
|
true
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
@ -1,3 +1,5 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { Observable } from 'rx';
|
||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
|
||||||
@ -24,41 +26,247 @@ function getCurrentHike(hikes = [{}], dashedName, currentHike) {
|
|||||||
}, currentHike || {});
|
}, currentHike || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findNextHike(hikes, id) {
|
||||||
|
if (!id) {
|
||||||
|
debug('find next hike no id provided');
|
||||||
|
return hikes[0];
|
||||||
|
}
|
||||||
|
const currentIndex = _.findIndex(hikes, ({ id: _id }) => _id === id);
|
||||||
|
return hikes[currentIndex + 1] || hikes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseQuestion(state) {
|
||||||
|
const oldHikesApp = state.hikesApp;
|
||||||
|
const hikesApp = {
|
||||||
|
...oldHikesApp,
|
||||||
|
isPressed: false,
|
||||||
|
delta: [0, 0],
|
||||||
|
mouse: oldHikesApp.isCorrect ?
|
||||||
|
oldHikesApp.mouse :
|
||||||
|
[0, 0]
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
|
||||||
export default Actions({
|
export default Actions({
|
||||||
// start fetching hikes
|
refs: { displayName: 'HikesActions' },
|
||||||
fetchHikes: null,
|
shouldBindMethods: true,
|
||||||
// set hikes on store
|
fetchHikes({ isPrimed, dashedName }) {
|
||||||
setHikes: null
|
|
||||||
})
|
|
||||||
.refs({ displayName: 'HikesActions' })
|
|
||||||
.init(({ instance: hikeActions, args: [services] }) => {
|
|
||||||
// set up hikes fetching
|
|
||||||
hikeActions.fetchHikes.subscribe(
|
|
||||||
({ isPrimed, dashedName }) => {
|
|
||||||
if (isPrimed) {
|
if (isPrimed) {
|
||||||
return hikeActions.setHikes({
|
return {
|
||||||
transform: (oldState) => {
|
transform: (state) => {
|
||||||
const { hikes } = oldState;
|
|
||||||
|
const { hikesApp: oldState } = state;
|
||||||
const currentHike = getCurrentHike(
|
const currentHike = getCurrentHike(
|
||||||
hikes,
|
oldState.hikes,
|
||||||
dashedName,
|
dashedName,
|
||||||
oldState.currentHike
|
oldState.currentHike
|
||||||
);
|
);
|
||||||
return Object.assign({}, oldState, { currentHike });
|
|
||||||
|
const hikesApp = { ...oldState, currentHike };
|
||||||
|
return Object.assign({}, state, { hikesApp });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.readService$('hikes', null, null)
|
||||||
|
.map(hikes => {
|
||||||
|
const currentHike = getCurrentHike(hikes, dashedName);
|
||||||
|
return {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = { ...state.hikesApp, currentHike, hikes };
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(err => Observable.just({
|
||||||
|
transform(state) { return { ...state, err }; }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleQuestions() {
|
||||||
|
return {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = {
|
||||||
|
...state.hikesApp,
|
||||||
|
showQuestions: !state.hikesApp.showQuestions,
|
||||||
|
currentQuestion: 1
|
||||||
|
};
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
hideInfo() {
|
||||||
|
return {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = { ...state.hikesApp, showInfo: false };
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
grabQuestion({ pressX, pressY, pageX, pageY }) {
|
||||||
|
const dx = pageX - pressX;
|
||||||
|
const dy = pageY - pressY;
|
||||||
|
|
||||||
|
const delta = [dx, dy];
|
||||||
|
const mouse = [pageX - dx, pageY - dy];
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = { ...state.hikesApp, isPressed: true, delta, mouse };
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
releaseQuestion() {
|
||||||
|
return { transform: releaseQuestion };
|
||||||
|
},
|
||||||
|
|
||||||
|
moveQuestion(mouse) {
|
||||||
|
return {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = { ...state.hikesApp, mouse };
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
answer({
|
||||||
|
answer,
|
||||||
|
userAnswer,
|
||||||
|
props: {
|
||||||
|
hike: { id, name, tests, challengeType },
|
||||||
|
currentQuestion,
|
||||||
|
username
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
|
||||||
|
// incorrect question
|
||||||
|
if (answer !== userAnswer) {
|
||||||
|
const startShake = {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = { ...state.hikesApp, showInfo: true, shake: true };
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeShake = {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = { ...state.hikesApp, shake: false };
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Observable
|
||||||
|
.just(removeShake)
|
||||||
|
.delay(500)
|
||||||
|
.startWith({ transform: releaseQuestion }, startShake);
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to next question
|
||||||
|
// index 0
|
||||||
|
if (tests[currentQuestion]) {
|
||||||
|
|
||||||
|
return Observable.just({
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = {
|
||||||
|
...state.hikesApp,
|
||||||
|
mouse: [0, 0],
|
||||||
|
showInfo: false
|
||||||
|
};
|
||||||
|
return { ...state, hikesApp };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.delay(300)
|
||||||
|
.startWith({
|
||||||
|
transform(state) {
|
||||||
|
|
||||||
|
const hikesApp = {
|
||||||
|
...state.hikesApp,
|
||||||
|
currentQuestion: currentQuestion + 1,
|
||||||
|
mouse: [ userAnswer ? 1000 : -1000, 0],
|
||||||
|
isPressed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...state, hikesApp };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
services.read('hikes', null, null, (err, hikes) => {
|
|
||||||
if (err) {
|
// challenge completed
|
||||||
debug('an error occurred fetching hikes', err);
|
const optimisticSave = username ?
|
||||||
|
this.post$('/completed-challenge', { id, name, challengeType }) :
|
||||||
|
Observable.just(true);
|
||||||
|
|
||||||
|
const correctAnswer = {
|
||||||
|
transform(state) {
|
||||||
|
const hikesApp = {
|
||||||
|
...state.hikesApp,
|
||||||
|
isCorrect: true,
|
||||||
|
isPressed: false,
|
||||||
|
delta: [0, 0],
|
||||||
|
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hikesApp
|
||||||
|
};
|
||||||
}
|
}
|
||||||
hikeActions.setHikes({
|
};
|
||||||
set: {
|
|
||||||
hikes: hikes,
|
return Observable.just({
|
||||||
currentHike: getCurrentHike(hikes, dashedName)
|
transform(state) {
|
||||||
|
const { hikes, currentHike: { id } } = state.hikesApp;
|
||||||
|
const currentHike = findNextHike(hikes, id);
|
||||||
|
|
||||||
|
// go to next route
|
||||||
|
state.location = {
|
||||||
|
action: 'PUSH',
|
||||||
|
pathname: currentHike && currentHike.dashedName ?
|
||||||
|
`/hikes/${ currentHike.dashedName }` :
|
||||||
|
'/hikes'
|
||||||
|
};
|
||||||
|
|
||||||
|
const hikesApp = {
|
||||||
|
...state.hikesApp,
|
||||||
|
currentHike,
|
||||||
|
showQuestions: false,
|
||||||
|
currentQuestion: 1,
|
||||||
|
mouse: [0, 0]
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
points: username ? state.points + 1 : state.points,
|
||||||
|
hikesApp
|
||||||
|
};
|
||||||
|
},
|
||||||
|
optimistic: optimisticSave
|
||||||
|
})
|
||||||
|
.delay(300)
|
||||||
|
.startWith(correctAnswer)
|
||||||
|
.catch(err => Observable.just({
|
||||||
|
transform(state) { return { ...state, err }; }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
resetHike() {
|
||||||
|
return {
|
||||||
|
transform(state) {
|
||||||
|
return { ...state,
|
||||||
|
hikesApp: {
|
||||||
|
...state.hikesApp,
|
||||||
|
currentQuestion: 1,
|
||||||
|
showQuestions: false,
|
||||||
|
showInfo: false,
|
||||||
|
mouse: [0, 0],
|
||||||
|
delta: [0, 0]
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { Store } from 'thundercats';
|
|
||||||
|
|
||||||
const initialValue = {
|
|
||||||
hikes: [],
|
|
||||||
currentHike: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Store({
|
|
||||||
refs: {
|
|
||||||
displayName: 'HikesStore',
|
|
||||||
value: initialValue
|
|
||||||
},
|
|
||||||
init({ instance: hikeStore, args: [cat] }) {
|
|
||||||
|
|
||||||
let { setHikes } = cat.getActions('hikesActions');
|
|
||||||
hikeStore.register(setHikes);
|
|
||||||
|
|
||||||
return hikeStore;
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,2 +1 @@
|
|||||||
export { default as HikesActions } from './Actions';
|
export { default as HikesActions } from './Actions';
|
||||||
export { default as HikesStore } from './Store';
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Hikes from './components/Hikes.jsx';
|
import Hikes from './components/Hikes.jsx';
|
||||||
import Lecture from './components/Lecture.jsx';
|
import Hike from './components/Hike.jsx';
|
||||||
import Question from './components/Question.jsx';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* show video /hikes/someVideo
|
* show video /hikes/someVideo
|
||||||
@ -12,9 +11,6 @@ export default {
|
|||||||
component: Hikes,
|
component: Hikes,
|
||||||
childRoutes: [{
|
childRoutes: [{
|
||||||
path: ':dashedName',
|
path: ':dashedName',
|
||||||
component: Lecture
|
component: Hike
|
||||||
}, {
|
|
||||||
path: ':dashedName/questions/:number',
|
|
||||||
component: Question
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
@ -11,13 +11,13 @@ const paypalIds = {
|
|||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'JobsStore',
|
store: 'appStore',
|
||||||
actions: [
|
actions: [
|
||||||
'jobActions',
|
'jobActions',
|
||||||
'appActions'
|
'appActions'
|
||||||
],
|
],
|
||||||
map({
|
map({ jobsApp: {
|
||||||
job: { id, isHighlighted } = {},
|
currentJob: { id, isHighlighted } = {},
|
||||||
buttonId = isHighlighted ?
|
buttonId = isHighlighted ?
|
||||||
paypalIds.highlighted :
|
paypalIds.highlighted :
|
||||||
paypalIds.regular,
|
paypalIds.regular,
|
||||||
@ -25,8 +25,8 @@ export default contain(
|
|||||||
discountAmount = 0,
|
discountAmount = 0,
|
||||||
promoCode = '',
|
promoCode = '',
|
||||||
promoApplied = false,
|
promoApplied = false,
|
||||||
promoName
|
promoName = ''
|
||||||
}) {
|
}}) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
@ -57,7 +57,7 @@ export default contain(
|
|||||||
|
|
||||||
goToJobBoard() {
|
goToJobBoard() {
|
||||||
const { appActions } = this.props;
|
const { appActions } = this.props;
|
||||||
appActions.updateRoute('/jobs');
|
appActions.goTo('/jobs');
|
||||||
},
|
},
|
||||||
|
|
||||||
renderDiscount(discountAmount) {
|
renderDiscount(discountAmount) {
|
||||||
|
@ -6,8 +6,14 @@ import ListJobs from './List.jsx';
|
|||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'jobsStore',
|
store: 'appStore',
|
||||||
|
map({ jobsApp: { jobs, showModal }}) {
|
||||||
|
return { jobs, showModal };
|
||||||
|
},
|
||||||
fetchAction: 'jobActions.getJobs',
|
fetchAction: 'jobActions.getJobs',
|
||||||
|
isPrimed({ jobs = [] }) {
|
||||||
|
return !!jobs.length;
|
||||||
|
},
|
||||||
actions: [
|
actions: [
|
||||||
'appActions',
|
'appActions',
|
||||||
'jobActions'
|
'jobActions'
|
||||||
@ -18,25 +24,19 @@ export default contain(
|
|||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
numOfFollowers: PropTypes.number,
|
|
||||||
appActions: PropTypes.object,
|
appActions: PropTypes.object,
|
||||||
jobActions: PropTypes.object,
|
jobActions: PropTypes.object,
|
||||||
jobs: PropTypes.array,
|
jobs: PropTypes.array,
|
||||||
showModal: PropTypes.bool
|
showModal: PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { jobActions } = this.props;
|
|
||||||
jobActions.getFollowers();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleJobClick(id) {
|
handleJobClick(id) {
|
||||||
const { appActions, jobActions } = this.props;
|
const { appActions, jobActions } = this.props;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
jobActions.findJob(id);
|
jobActions.findJob(id);
|
||||||
appActions.updateRoute(`/jobs/${id}`);
|
appActions.goTo(`/jobs/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderList(handleJobClick, jobs) {
|
renderList(handleJobClick, jobs) {
|
||||||
@ -84,7 +84,7 @@ export default contain(
|
|||||||
<Button
|
<Button
|
||||||
className='signup-btn btn-block btn-cta'
|
className='signup-btn btn-block btn-cta'
|
||||||
onClick={ ()=> {
|
onClick={ ()=> {
|
||||||
appActions.updateRoute('/jobs/new');
|
appActions.goTo('/jobs/new');
|
||||||
}}>
|
}}>
|
||||||
Post a job: $1,000
|
Post a job: $1,000
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -103,9 +103,9 @@ function makeRequired(validator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default contain({
|
export default contain({
|
||||||
|
store: 'appStore',
|
||||||
actions: 'jobActions',
|
actions: 'jobActions',
|
||||||
store: 'jobsStore',
|
map({ jobsApp: { form = {} } }) {
|
||||||
map({ form = {} }) {
|
|
||||||
const {
|
const {
|
||||||
position,
|
position,
|
||||||
locale,
|
locale,
|
||||||
@ -115,7 +115,7 @@ export default contain({
|
|||||||
logo,
|
logo,
|
||||||
company,
|
company,
|
||||||
isFrontEndCert = true,
|
isFrontEndCert = true,
|
||||||
isFullStackCert,
|
isBackEndCert,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
isRemoteOk,
|
isRemoteOk,
|
||||||
howToApply
|
howToApply
|
||||||
@ -132,7 +132,7 @@ export default contain({
|
|||||||
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
||||||
howToApply: formatValue(howToApply, makeRequired(isAscii)),
|
howToApply: formatValue(howToApply, makeRequired(isAscii)),
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isFullStackCert
|
isBackEndCert
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
subscribeOnWillMount() {
|
subscribeOnWillMount() {
|
||||||
@ -154,7 +154,7 @@ export default contain({
|
|||||||
isHighlighted: PropTypes.object,
|
isHighlighted: PropTypes.object,
|
||||||
isRemoteOk: PropTypes.object,
|
isRemoteOk: PropTypes.object,
|
||||||
isFrontEndCert: PropTypes.bool,
|
isFrontEndCert: PropTypes.bool,
|
||||||
isFullStackCert: PropTypes.bool,
|
isBackEndCert: PropTypes.bool,
|
||||||
howToApply: PropTypes.object
|
howToApply: PropTypes.object
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -171,7 +171,11 @@ export default contain({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!valid || !pros.isFrontEndCert && !pros.isFullStackCert ) {
|
if (
|
||||||
|
!valid ||
|
||||||
|
!pros.isFrontEndCert &&
|
||||||
|
!pros.isBackEndCert
|
||||||
|
) {
|
||||||
debug('form not valid');
|
debug('form not valid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -188,7 +192,7 @@ export default contain({
|
|||||||
logo,
|
logo,
|
||||||
company,
|
company,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isFullStackCert,
|
isBackEndCert,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
isRemoteOk,
|
isRemoteOk,
|
||||||
howToApply
|
howToApply
|
||||||
@ -207,7 +211,7 @@ export default contain({
|
|||||||
isRemoteOk: !!isRemoteOk.value,
|
isRemoteOk: !!isRemoteOk.value,
|
||||||
howToApply: inHTMLData(howToApply.value),
|
howToApply: inHTMLData(howToApply.value),
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isFullStackCert
|
isBackEndCert
|
||||||
};
|
};
|
||||||
|
|
||||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
||||||
@ -237,7 +241,7 @@ export default contain({
|
|||||||
handleCertClick(name) {
|
handleCertClick(name) {
|
||||||
const { jobActions: { handleForm } } = this.props;
|
const { jobActions: { handleForm } } = this.props;
|
||||||
const otherButton = name === 'isFrontEndCert' ?
|
const otherButton = name === 'isFrontEndCert' ?
|
||||||
'isFullStackCert' :
|
'isBackEndCert' :
|
||||||
'isFrontEndCert';
|
'isFrontEndCert';
|
||||||
|
|
||||||
handleForm({
|
handleForm({
|
||||||
@ -259,7 +263,7 @@ export default contain({
|
|||||||
isRemoteOk,
|
isRemoteOk,
|
||||||
howToApply,
|
howToApply,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isFullStackCert,
|
isBackEndCert,
|
||||||
jobActions: { handleForm }
|
jobActions: { handleForm }
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -306,13 +310,13 @@ export default contain({
|
|||||||
<div className='button-spacer' />
|
<div className='button-spacer' />
|
||||||
<Row>
|
<Row>
|
||||||
<Button
|
<Button
|
||||||
className={ isFullStackCert ? 'active' : ''}
|
className={ isBackEndCert ? 'active' : ''}
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
if (!isFullStackCert) {
|
if (!isBackEndCert) {
|
||||||
this.handleCertClick('isFullStackCert');
|
this.handleCertClick('isBackEndCert');
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<h4>Full Stack Development Certified</h4>
|
<h4>Back End Development Certified</h4>
|
||||||
You can expect each applicant to have a code
|
You can expect each applicant to have a code
|
||||||
portfolio using the following technologies:
|
portfolio using the following technologies:
|
||||||
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
||||||
|
@ -8,12 +8,12 @@ import JobNotFound from './JobNotFound.jsx';
|
|||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'JobsStore',
|
store: 'appStore',
|
||||||
actions: [
|
actions: [
|
||||||
'appActions',
|
'appActions',
|
||||||
'jobActions'
|
'jobActions'
|
||||||
],
|
],
|
||||||
map({ form: job = {} }) {
|
map({ jobsApp: { form: job = {} } }) {
|
||||||
return { job };
|
return { job };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -32,7 +32,7 @@ export default contain(
|
|||||||
const { appActions, job } = this.props;
|
const { appActions, job } = this.props;
|
||||||
// redirect user in client
|
// redirect user in client
|
||||||
if (!job || !job.position || !job.description) {
|
if (!job || !job.position || !job.description) {
|
||||||
appActions.updateRoute('/jobs/new');
|
appActions.goTo('/jobs/new');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -9,25 +9,25 @@ import { isJobValid } from '../utils';
|
|||||||
function shouldShowApply(
|
function shouldShowApply(
|
||||||
{
|
{
|
||||||
isFrontEndCert: isFrontEndCertReq = false,
|
isFrontEndCert: isFrontEndCertReq = false,
|
||||||
isFullStackCert: isFullStackCertReq = false
|
isBackEndCert: isBackEndCertReq = false
|
||||||
}, {
|
}, {
|
||||||
isFrontEndCert = false,
|
isFrontEndCert = false,
|
||||||
isFullStackCert = false
|
isBackEndCert = false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
return (!isFrontEndCertReq && !isFullStackCertReq) ||
|
return (!isFrontEndCertReq && !isBackEndCertReq) ||
|
||||||
(isFullStackCertReq && isFullStackCert) ||
|
(isBackEndCertReq && isBackEndCert) ||
|
||||||
(isFrontEndCertReq && isFrontEndCert);
|
(isFrontEndCertReq && isFrontEndCert);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMessage(
|
function generateMessage(
|
||||||
{
|
{
|
||||||
isFrontEndCert: isFrontEndCertReq = false,
|
isFrontEndCert: isFrontEndCertReq = false,
|
||||||
isFullStackCert: isFullStackCertReq = false
|
isBackEndCert: isBackEndCertReq = false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isFrontEndCert = false,
|
isFrontEndCert = false,
|
||||||
isFullStackCert = false,
|
isBackEndCert = false,
|
||||||
isSignedIn = false
|
isSignedIn = false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@ -39,39 +39,40 @@ function generateMessage(
|
|||||||
return 'This employer requires Free Code Camp’s Front ' +
|
return 'This employer requires Free Code Camp’s Front ' +
|
||||||
'End Development Certification in order to apply';
|
'End Development Certification in order to apply';
|
||||||
}
|
}
|
||||||
if (isFullStackCertReq && !isFullStackCert) {
|
if (isBackEndCertReq && !isBackEndCert) {
|
||||||
return 'This employer requires Free Code Camp’s Full ' +
|
return 'This employer requires Free Code Camp’s Back ' +
|
||||||
'Stack Development Certification in order to apply';
|
'End Development Certification in order to apply';
|
||||||
}
|
}
|
||||||
if (isFrontEndCertReq && isFrontEndCertReq) {
|
if (isFrontEndCertReq && isFrontEndCertReq) {
|
||||||
return 'This employer requires the Front End Development Certification. ' +
|
return 'This employer requires the Front End Development Certification. ' +
|
||||||
"You've earned it, so feel free to apply.";
|
"You've earned it, so feel free to apply.";
|
||||||
}
|
}
|
||||||
return 'This employer requires the Full Stack Development Certification. ' +
|
return 'This employer requires the Back End Development Certification. ' +
|
||||||
"You've earned it, so feel free to apply.";
|
"You've earned it, so feel free to apply.";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
stores: ['appStore', 'jobsStore'],
|
store: 'appStore',
|
||||||
fetchWaitFor: 'jobsStore',
|
|
||||||
fetchAction: 'jobActions.getJob',
|
fetchAction: 'jobActions.getJob',
|
||||||
combineLatest(
|
map({
|
||||||
{ username, isFrontEndCert, isFullStackCert },
|
username,
|
||||||
{ currentJob }
|
isFrontEndCert,
|
||||||
) {
|
isBackEndCert,
|
||||||
|
jobsApp: { currentJob }
|
||||||
|
}) {
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
job: currentJob,
|
job: currentJob,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isFullStackCert
|
isBackEndCert
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPayload({ params: { id }, job = {} }) {
|
getPayload({ params: { id } }) {
|
||||||
return {
|
return id;
|
||||||
id,
|
},
|
||||||
isPrimed: job.id === id
|
isPrimed({ params: { id } = {}, job = {} }) {
|
||||||
};
|
return job.id === id;
|
||||||
},
|
},
|
||||||
// using es6 destructuring
|
// using es6 destructuring
|
||||||
shouldContainerFetch({ job = {} }, { params: { id } }
|
shouldContainerFetch({ job = {} }, { params: { id } }
|
||||||
@ -84,7 +85,7 @@ export default contain(
|
|||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
job: PropTypes.object,
|
job: PropTypes.object,
|
||||||
isFullStackCert: PropTypes.bool,
|
isBackEndCert: PropTypes.bool,
|
||||||
isFrontEndCert: PropTypes.bool,
|
isFrontEndCert: PropTypes.bool,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
},
|
},
|
||||||
@ -101,7 +102,7 @@ export default contain(
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isFullStackCert,
|
isBackEndCert,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
job,
|
job,
|
||||||
username
|
username
|
||||||
@ -115,12 +116,12 @@ export default contain(
|
|||||||
|
|
||||||
const showApply = shouldShowApply(
|
const showApply = shouldShowApply(
|
||||||
job,
|
job,
|
||||||
{ isFrontEndCert, isFullStackCert }
|
{ isFrontEndCert, isBackEndCert }
|
||||||
);
|
);
|
||||||
|
|
||||||
const message = generateMessage(
|
const message = generateMessage(
|
||||||
job,
|
job,
|
||||||
{ isFrontEndCert, isFullStackCert, isSignedIn }
|
{ isFrontEndCert, isBackEndCert, isSignedIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import debugFactory from 'debug';
|
import { Observable } from 'rx';
|
||||||
import { jsonp$ } from '../../../../utils/jsonp$';
|
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { nameSpacedTransformer } from '../../../../utils';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:jobs:actions');
|
|
||||||
const assign = Object.assign;
|
const assign = Object.assign;
|
||||||
|
const jobsTranformer = nameSpacedTransformer('jobsApp');
|
||||||
|
const noOper = { transform: () => {} };
|
||||||
|
|
||||||
export default Actions({
|
export default Actions({
|
||||||
setJobs: null,
|
refs: { displayName: 'JobActions' },
|
||||||
|
shouldBindMethods: true,
|
||||||
// findJob assumes that the job is already in the list of jobs
|
// findJob assumes that the job is already in the list of jobs
|
||||||
findJob(id) {
|
findJob(id) {
|
||||||
return oldState => {
|
return {
|
||||||
|
transform: jobsTranformer(oldState => {
|
||||||
const { currentJob = {}, jobs = [] } = oldState;
|
const { currentJob = {}, jobs = [] } = oldState;
|
||||||
// currentJob already set
|
// currentJob already set
|
||||||
// do nothing
|
// do nothing
|
||||||
@ -29,23 +32,71 @@ export default Actions({
|
|||||||
return foundJob ?
|
return foundJob ?
|
||||||
assign({}, oldState, { currentJob: foundJob }) :
|
assign({}, oldState, { currentJob: foundJob }) :
|
||||||
null;
|
null;
|
||||||
|
})
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setError: null,
|
saveJobToDb({ goTo, job }) {
|
||||||
getJob: null,
|
return this.createService$('jobs', { job })
|
||||||
saveJobToDb: null,
|
.map(job => ({
|
||||||
getJobs(params) {
|
transform(state) {
|
||||||
return { params };
|
state.location = {
|
||||||
|
action: 'PUSH',
|
||||||
|
pathname: goTo
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
jobsApp: {
|
||||||
|
...state.jobs,
|
||||||
|
currentJob: job
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.catch(err => Observable.just({
|
||||||
|
transform(state) {
|
||||||
|
return { ...state, err };
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
getJob(id) {
|
||||||
|
return this.readService$('jobs', { id })
|
||||||
|
.map(job => ({
|
||||||
|
transform: jobsTranformer(state => {
|
||||||
|
return { ...state, currentJob: job };
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.catch(err => Observable.just({
|
||||||
|
transform(state) {
|
||||||
|
return { ...state, err };
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
getJobs() {
|
||||||
|
return this.readService$('jobs')
|
||||||
|
.map(jobs => ({
|
||||||
|
transform: jobsTranformer(state => {
|
||||||
|
return { ...state, jobs };
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.catch(err => Observable.just({
|
||||||
|
transform(state) {
|
||||||
|
return { ...state, err };
|
||||||
|
}
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
openModal() {
|
openModal() {
|
||||||
return { showModal: true };
|
return {
|
||||||
|
transform: jobsTranformer(state => ({ ...state, showModal: true }))
|
||||||
|
};
|
||||||
},
|
},
|
||||||
closeModal() {
|
closeModal() {
|
||||||
return { showModal: false };
|
return {
|
||||||
|
transform: jobsTranformer(state => ({ ...state, showModal: false }))
|
||||||
|
};
|
||||||
},
|
},
|
||||||
handleForm(value) {
|
handleForm(value) {
|
||||||
return {
|
return {
|
||||||
transform(oldState) {
|
transform: jobsTranformer(oldState => {
|
||||||
const { form } = oldState;
|
const { form } = oldState;
|
||||||
const newState = assign({}, oldState);
|
const newState = assign({}, oldState);
|
||||||
newState.form = assign(
|
newState.form = assign(
|
||||||
@ -54,123 +105,31 @@ export default Actions({
|
|||||||
value
|
value
|
||||||
);
|
);
|
||||||
return newState;
|
return newState;
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
saveForm: null,
|
saveForm: null,
|
||||||
getSavedForm: null,
|
|
||||||
clearSavedForm: null,
|
clearSavedForm: null,
|
||||||
setForm(form) {
|
getSavedForm() {
|
||||||
return { form };
|
const form = store.get('newJob');
|
||||||
},
|
if (form && !Array.isArray(form) && typeof form === 'object') {
|
||||||
getFollowers: null,
|
return {
|
||||||
setFollowersCount(numOfFollowers) {
|
transform: jobsTranformer(state => {
|
||||||
return { numOfFollowers };
|
return { ...state, form };
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return noOper;
|
||||||
},
|
},
|
||||||
setPromoCode({ target: { value = '' }} = {}) {
|
setPromoCode({ target: { value = '' }} = {}) {
|
||||||
return { promoCode: value.replace(/[^\d\w\s]/, '') };
|
|
||||||
},
|
|
||||||
applyCode: null,
|
|
||||||
clearPromo(foo, undef) {
|
|
||||||
return {
|
return {
|
||||||
price: undef,
|
transform: jobsTranformer(state => ({
|
||||||
buttonId: undef,
|
...state,
|
||||||
discountAmount: undef,
|
promoCode: value.replace(/[^\d\w\s]/, '')
|
||||||
promoCode: undef,
|
}))
|
||||||
promoApplied: false,
|
|
||||||
promoName: undef
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
applyPromo({
|
applyCode({ id, code = '', type = null}) {
|
||||||
fullPrice: price,
|
|
||||||
buttonId,
|
|
||||||
discountAmount,
|
|
||||||
code: promoCode,
|
|
||||||
name: promoName
|
|
||||||
} = {}) {
|
|
||||||
return {
|
|
||||||
price,
|
|
||||||
buttonId,
|
|
||||||
discountAmount,
|
|
||||||
promoCode,
|
|
||||||
promoApplied: true,
|
|
||||||
promoName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.refs({ displayName: 'JobActions' })
|
|
||||||
.init(({ instance: jobActions, args: [cat, services] }) => {
|
|
||||||
jobActions.getJobs.subscribe(() => {
|
|
||||||
services.read('jobs', null, null, (err, jobs) => {
|
|
||||||
if (err) {
|
|
||||||
debug('job services experienced an issue', err);
|
|
||||||
return jobActions.setError({ err });
|
|
||||||
}
|
|
||||||
jobActions.setJobs({ jobs });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.getJob.subscribe(({ id, isPrimed }) => {
|
|
||||||
// job is already set, do nothing.
|
|
||||||
if (isPrimed) {
|
|
||||||
debug('job is primed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
services.read('jobs', { id }, null, (err, job) => {
|
|
||||||
if (err) {
|
|
||||||
debug('job services experienced an issue', err);
|
|
||||||
return jobActions.setError({ err });
|
|
||||||
}
|
|
||||||
if (job) {
|
|
||||||
jobActions.setJobs({ currentJob: job });
|
|
||||||
}
|
|
||||||
jobActions.setJobs({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.saveForm.subscribe((form) => {
|
|
||||||
store.set('newJob', form);
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.getSavedForm.subscribe(() => {
|
|
||||||
const job = store.get('newJob');
|
|
||||||
if (job && !Array.isArray(job) && typeof job === 'object') {
|
|
||||||
jobActions.setForm(job);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.clearSavedForm.subscribe(() => {
|
|
||||||
store.remove('newJob');
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.saveJobToDb.subscribe(({ goTo, job }) => {
|
|
||||||
const appActions = cat.getActions('appActions');
|
|
||||||
services.create('jobs', { job }, null, (err, job) => {
|
|
||||||
if (err) {
|
|
||||||
debug('job services experienced an issue', err);
|
|
||||||
return jobActions.setError(err);
|
|
||||||
}
|
|
||||||
jobActions.setJobs({ job });
|
|
||||||
appActions.updateRoute(goTo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.getFollowers.subscribe(() => {
|
|
||||||
const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' +
|
|
||||||
'info.json?lang=en&screen_names=CamperJobs' +
|
|
||||||
'&callback=JSONPCallback';
|
|
||||||
|
|
||||||
jsonp$(url)
|
|
||||||
.map(({ response }) => {
|
|
||||||
return response[0]['followers_count'];
|
|
||||||
})
|
|
||||||
.subscribe(
|
|
||||||
count => jobActions.setFollowersCount(count),
|
|
||||||
err => jobActions.setError(err)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
jobActions.applyCode.subscribe(({ id, code = '', type = null}) => {
|
|
||||||
const body = {
|
const body = {
|
||||||
id,
|
id,
|
||||||
code: code.replace(/[^\d\w\s]/, '')
|
code: code.replace(/[^\d\w\s]/, '')
|
||||||
@ -178,18 +137,62 @@ export default Actions({
|
|||||||
if (type) {
|
if (type) {
|
||||||
body.type = type;
|
body.type = type;
|
||||||
}
|
}
|
||||||
postJSON$('/api/promos/getButton', body)
|
return this.postJSON$('/api/promos/getButton', body)
|
||||||
.pluck('response')
|
.pluck('response')
|
||||||
.subscribe(
|
.map(({ promo }) => {
|
||||||
({ promo }) => {
|
if (!promo || !promo.buttonId) {
|
||||||
if (promo && promo.buttonId) {
|
return noOper;
|
||||||
jobActions.applyPromo(promo);
|
|
||||||
}
|
}
|
||||||
jobActions.setError(new Error('no promo found'));
|
const {
|
||||||
|
fullPrice: price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
code: promoCode,
|
||||||
|
name: promoName
|
||||||
|
} = promo;
|
||||||
|
|
||||||
|
return {
|
||||||
|
transform: jobsTranformer(state => ({
|
||||||
|
...state,
|
||||||
|
price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
promoCode,
|
||||||
|
promoApplied: true,
|
||||||
|
promoName
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(err => Observable.just({
|
||||||
|
transform(state) {
|
||||||
|
return { ...state, err };
|
||||||
|
}
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
jobActions.setError
|
clearPromo() {
|
||||||
);
|
return {
|
||||||
|
/* eslint-disable no-undefined */
|
||||||
|
transform: jobsTranformer(state => ({
|
||||||
|
...state,
|
||||||
|
price: undefined,
|
||||||
|
buttonId: undefined,
|
||||||
|
discountAmount: undefined,
|
||||||
|
promoCode: undefined,
|
||||||
|
promoApplied: false,
|
||||||
|
promoName: undefined
|
||||||
|
}))
|
||||||
|
/* eslint-enable no-undefined */
|
||||||
|
};
|
||||||
|
},
|
||||||
|
init({ instance: jobActions }) {
|
||||||
|
jobActions.saveForm.subscribe((form) => {
|
||||||
|
store.set('newJob', form);
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.clearSavedForm.subscribe(() => {
|
||||||
|
store.remove('newJob');
|
||||||
});
|
});
|
||||||
|
|
||||||
return jobActions;
|
return jobActions;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import { Store } from 'thundercats';
|
|
||||||
|
|
||||||
const {
|
|
||||||
createRegistrar,
|
|
||||||
setter,
|
|
||||||
transformer
|
|
||||||
} = Store;
|
|
||||||
|
|
||||||
export default Store({
|
|
||||||
refs: {
|
|
||||||
displayName: 'JobsStore',
|
|
||||||
value: { showModal: false }
|
|
||||||
},
|
|
||||||
init({ instance: jobsStore, args: [cat] }) {
|
|
||||||
const {
|
|
||||||
setJobs,
|
|
||||||
findJob,
|
|
||||||
setError,
|
|
||||||
openModal,
|
|
||||||
closeModal,
|
|
||||||
handleForm,
|
|
||||||
setForm,
|
|
||||||
setFollowersCount,
|
|
||||||
setPromoCode,
|
|
||||||
applyPromo,
|
|
||||||
clearPromo
|
|
||||||
} = cat.getActions('JobActions');
|
|
||||||
const register = createRegistrar(jobsStore);
|
|
||||||
register(setter(setJobs));
|
|
||||||
register(setter(setError));
|
|
||||||
register(setter(openModal));
|
|
||||||
register(setter(closeModal));
|
|
||||||
register(setter(setForm));
|
|
||||||
register(setter(setPromoCode));
|
|
||||||
register(setter(applyPromo));
|
|
||||||
register(setter(clearPromo));
|
|
||||||
register(setter(setFollowersCount));
|
|
||||||
|
|
||||||
register(transformer(findJob));
|
|
||||||
register(handleForm);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,2 +1 @@
|
|||||||
export { default as JobActions } from './Actions';
|
export { default as JobActions } from './Actions';
|
||||||
export { default as JobsStore } from './Store';
|
|
||||||
|
@ -74,6 +74,11 @@
|
|||||||
"defaut": false,
|
"defaut": false,
|
||||||
"description": "Camper must be front end certified to apply"
|
"description": "Camper must be front end certified to apply"
|
||||||
},
|
},
|
||||||
|
"isBackEndCert": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Camper must be back end certified to apply"
|
||||||
|
},
|
||||||
"isFullStackCert": {
|
"isFullStackCert": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { AnonymousObservable, helpers } from 'rx';
|
import { Observable, AnonymousObservable, helpers } from 'rx';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:ajax$');
|
const debug = debugFactory('freecc:ajax$');
|
||||||
const root = typeof window !== 'undefined' ? window : {};
|
const root = typeof window !== 'undefined' ? window : {};
|
||||||
@ -147,8 +147,12 @@ export function ajax$(options) {
|
|||||||
var processResponse = function(xhr, e) {
|
var processResponse = function(xhr, e) {
|
||||||
var status = xhr.status === 1223 ? 204 : xhr.status;
|
var status = xhr.status === 1223 ? 204 : xhr.status;
|
||||||
if ((status >= 200 && status <= 300) || status === 0 || status === '') {
|
if ((status >= 200 && status <= 300) || status === 0 || status === '') {
|
||||||
|
try {
|
||||||
observer.onNext(normalizeSuccess(e, xhr, settings));
|
observer.onNext(normalizeSuccess(e, xhr, settings));
|
||||||
observer.onCompleted();
|
observer.onCompleted();
|
||||||
|
} catch (err) {
|
||||||
|
observer.onError(err);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
observer.onError(normalizeError(e, xhr, 'error'));
|
observer.onError(normalizeError(e, xhr, 'error'));
|
||||||
}
|
}
|
||||||
@ -228,8 +232,8 @@ export function ajax$(options) {
|
|||||||
settings.hasContent && settings.body
|
settings.hasContent && settings.body
|
||||||
);
|
);
|
||||||
xhr.send(settings.hasContent && settings.body || null);
|
xhr.send(settings.hasContent && settings.body || null);
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
observer.onError(e);
|
observer.onError(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return function() {
|
return function() {
|
||||||
@ -247,13 +251,25 @@ export function ajax$(options) {
|
|||||||
* from the Ajax POST.
|
* from the Ajax POST.
|
||||||
*/
|
*/
|
||||||
export function post$(url, body) {
|
export function post$(url, body) {
|
||||||
|
try {
|
||||||
|
body = JSON.stringify(body);
|
||||||
|
} catch (e) {
|
||||||
|
return Observable.throw(e);
|
||||||
|
}
|
||||||
|
|
||||||
return ajax$({ url, body, method: 'POST' });
|
return ajax$({ url, body, method: 'POST' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postJSON$(url, body) {
|
export function postJSON$(url, body) {
|
||||||
|
try {
|
||||||
|
body = JSON.stringify(body);
|
||||||
|
} catch (e) {
|
||||||
|
return Observable.throw(e);
|
||||||
|
}
|
||||||
|
|
||||||
return ajax$({
|
return ajax$({
|
||||||
url,
|
url,
|
||||||
body: JSON.stringify(body),
|
body,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
16
common/utils/index.js
Normal file
16
common/utils/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export function nameSpacedTransformer(ns, transformer) {
|
||||||
|
if (!transformer) {
|
||||||
|
return nameSpacedTransformer.bind(null, ns);
|
||||||
|
}
|
||||||
|
return (state) => {
|
||||||
|
const newState = transformer(state[ns]);
|
||||||
|
|
||||||
|
// nothing has changed
|
||||||
|
// noop
|
||||||
|
if (!newState || newState === state[ns]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, [ns]: newState };
|
||||||
|
};
|
||||||
|
}
|
11
gulpfile.js
11
gulpfile.js
@ -183,7 +183,7 @@ function delRev(dest, manifestName) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.task('serve', function(cb) {
|
gulp.task('serve', ['build-manifest'], function(cb) {
|
||||||
var called = false;
|
var called = false;
|
||||||
nodemon({
|
nodemon({
|
||||||
script: paths.server,
|
script: paths.server,
|
||||||
@ -483,6 +483,10 @@ function buildManifest() {
|
|||||||
|
|
||||||
var buildDependents = ['less', 'js', 'dependents'];
|
var buildDependents = ['less', 'js', 'dependents'];
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
buildDependents.push('pack-watch-manifest');
|
||||||
|
}
|
||||||
|
|
||||||
gulp.task('build-manifest', buildDependents, function() {
|
gulp.task('build-manifest', buildDependents, function() {
|
||||||
return buildManifest();
|
return buildManifest();
|
||||||
});
|
});
|
||||||
@ -505,9 +509,9 @@ var watchDependents = [
|
|||||||
'dependents',
|
'dependents',
|
||||||
'serve',
|
'serve',
|
||||||
'sync',
|
'sync',
|
||||||
'build-manifest',
|
|
||||||
'pack-watch',
|
'pack-watch',
|
||||||
'pack-watch-manifest'
|
'pack-watch-manifest',
|
||||||
|
'build-manifest'
|
||||||
];
|
];
|
||||||
|
|
||||||
gulp.task('reload', function() {
|
gulp.task('reload', function() {
|
||||||
@ -533,6 +537,7 @@ gulp.task('default', [
|
|||||||
'serve',
|
'serve',
|
||||||
'pack-watch',
|
'pack-watch',
|
||||||
'pack-watch-manifest',
|
'pack-watch-manifest',
|
||||||
|
'build-manifest-watch',
|
||||||
'watch',
|
'watch',
|
||||||
'sync'
|
'sync'
|
||||||
]);
|
]);
|
||||||
|
@ -115,9 +115,10 @@
|
|||||||
"rx": "^4.0.0",
|
"rx": "^4.0.0",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
"sort-keys": "^1.1.1",
|
"sort-keys": "^1.1.1",
|
||||||
|
"stampit": "^2.1.1",
|
||||||
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
||||||
"thundercats": "^3.0.0",
|
"thundercats": "^3.1.0",
|
||||||
"thundercats-react": "~0.4.0",
|
"thundercats-react": "~0.5.1",
|
||||||
"twit": "^2.1.1",
|
"twit": "^2.1.1",
|
||||||
"uglify-js": "^2.5.0",
|
"uglify-js": "^2.5.0",
|
||||||
"url-regex": "^3.0.0",
|
"url-regex": "^3.0.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Computer Basics",
|
"name": "Computer Basics",
|
||||||
"order": 0.050,
|
"order": 0,
|
||||||
"time": "3h",
|
"time": "3h",
|
||||||
"challenges": [
|
"challenges": [
|
||||||
{
|
{
|
||||||
|
@ -3,8 +3,10 @@ import { RoutingContext } from 'react-router';
|
|||||||
import Fetchr from 'fetchr';
|
import Fetchr from 'fetchr';
|
||||||
import { createLocation } from 'history';
|
import { createLocation } from 'history';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
import { dehydrate } from 'thundercats';
|
||||||
|
import { renderToString$ } from 'thundercats-react';
|
||||||
|
|
||||||
import { app$ } from '../../common/app';
|
import { app$ } from '../../common/app';
|
||||||
import { RenderToString } from 'thundercats-react';
|
|
||||||
|
|
||||||
const debug = debugFactory('freecc:react-server');
|
const debug = debugFactory('freecc:react-server');
|
||||||
|
|
||||||
@ -12,14 +14,13 @@ const debug = debugFactory('freecc:react-server');
|
|||||||
// remove their individual controllers
|
// remove their individual controllers
|
||||||
const routes = [
|
const routes = [
|
||||||
'/jobs',
|
'/jobs',
|
||||||
'/jobs/*'
|
'/jobs/*',
|
||||||
];
|
|
||||||
|
|
||||||
const devRoutes = [
|
|
||||||
'/hikes',
|
'/hikes',
|
||||||
'/hikes/*'
|
'/hikes/*'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const devRoutes = [];
|
||||||
|
|
||||||
export default function reactSubRouter(app) {
|
export default function reactSubRouter(app) {
|
||||||
var router = app.loopback.Router();
|
var router = app.loopback.Router();
|
||||||
|
|
||||||
@ -51,20 +52,28 @@ export default function reactSubRouter(app) {
|
|||||||
return !!props;
|
return !!props;
|
||||||
})
|
})
|
||||||
.flatMap(function({ props, AppCat }) {
|
.flatMap(function({ props, AppCat }) {
|
||||||
// call thundercats renderToString
|
const cat = AppCat(null, services);
|
||||||
// prefetches data and sets up it up for current state
|
debug('render react markup and pre-fetch data');
|
||||||
debug('rendering to string');
|
const store = cat.getStore('appStore');
|
||||||
return RenderToString(
|
|
||||||
AppCat(null, services),
|
// primes store to observe action changes
|
||||||
|
// cleaned up by cat.dispose further down
|
||||||
|
store.subscribe(() => {});
|
||||||
|
|
||||||
|
return renderToString$(
|
||||||
|
cat,
|
||||||
React.createElement(RoutingContext, props)
|
React.createElement(RoutingContext, props)
|
||||||
|
)
|
||||||
|
.flatMap(
|
||||||
|
dehydrate(cat),
|
||||||
|
({ markup }, data) => ({ markup, data, cat })
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
// makes sure we only get one onNext and closes subscription
|
.flatMap(function({ data, markup, cat }) {
|
||||||
.flatMap(function({ data, markup }) {
|
debug('react markup rendered, data fetched');
|
||||||
debug('react rendered');
|
cat.dispose();
|
||||||
const { title } = data.AppStore;
|
const { title } = data.AppStore;
|
||||||
res.expose(data, 'data');
|
res.expose(data, 'data');
|
||||||
// now render jade file with markup injected from react
|
|
||||||
return res.render$(
|
return res.render$(
|
||||||
'layout-react',
|
'layout-react',
|
||||||
{ markup, title }
|
{ markup, title }
|
||||||
@ -72,7 +81,7 @@ export default function reactSubRouter(app) {
|
|||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
function(markup) {
|
function(markup) {
|
||||||
debug('jade rendered');
|
debug('html rendered and ready to send');
|
||||||
res.send(markup);
|
res.send(markup);
|
||||||
},
|
},
|
||||||
next
|
next
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
|
|
||||||
const trusted = [
|
let trusted = [
|
||||||
"'self'"
|
"'self'"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
trusted.push('ws://localhost:3001');
|
||||||
|
}
|
||||||
|
|
||||||
export default function csp() {
|
export default function csp() {
|
||||||
return helmet.csp({
|
return helmet.csp({
|
||||||
defaultSrc: trusted,
|
defaultSrc: trusted,
|
||||||
|
@ -11,7 +11,7 @@ export default function hikesService(app) {
|
|||||||
read: (req, resource, params, config, cb) => {
|
read: (req, resource, params, config, cb) => {
|
||||||
const query = {
|
const query = {
|
||||||
where: { challengeType: '6' },
|
where: { challengeType: '6' },
|
||||||
order: 'suborder ASC'
|
order: ['order ASC', 'suborder ASC' ]
|
||||||
};
|
};
|
||||||
|
|
||||||
debug('params', params);
|
debug('params', params);
|
||||||
|
Reference in New Issue
Block a user