@ -6,9 +6,10 @@ import debugFactory from 'debug';
|
||||
import { Router } from 'react-router';
|
||||
import { createLocation, createHistory } from 'history';
|
||||
import { hydrate } from 'thundercats';
|
||||
import { Render } from 'thundercats-react';
|
||||
import { render$ } from 'thundercats-react';
|
||||
|
||||
import { app$ } from '../common/app';
|
||||
import synchroniseHistory from './synchronise-history';
|
||||
|
||||
const debug = debugFactory('fcc:client');
|
||||
const DOMContianer = document.getElementById('fcc');
|
||||
@ -23,56 +24,55 @@ const appLocation = createLocation(
|
||||
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
|
||||
app$({ history, location: appLocation })
|
||||
.flatMap(
|
||||
({ AppCat }) => {
|
||||
// instantiate the cat with service
|
||||
const appCat = AppCat(null, services);
|
||||
const appCat = AppCat(null, services, history);
|
||||
// hydrate the stores
|
||||
return hydrate(appCat, catState)
|
||||
.map(() => appCat);
|
||||
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 appActions = appCat.getActions('appActions');
|
||||
const { updateLocation, goTo, goBack } = appCat.getActions('appActions');
|
||||
const appStore$ = appCat.getStore('appStore');
|
||||
|
||||
location$(history)
|
||||
.pluck('pathname')
|
||||
const routerState$ = appStore$
|
||||
.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()
|
||||
.doOnNext(route => debug('route change', route))
|
||||
.subscribe(route => appActions.updateRoute(route));
|
||||
.subscribe(err => console.error(err));
|
||||
|
||||
appActions.goBack.subscribe(function() {
|
||||
history.goBack();
|
||||
});
|
||||
|
||||
appActions
|
||||
.updateRoute
|
||||
.pluck('route')
|
||||
.doOnNext(route => debug('update route', route))
|
||||
.subscribe(function(route) {
|
||||
history.pushState(null, route);
|
||||
});
|
||||
synchroniseHistory(
|
||||
history,
|
||||
updateLocation,
|
||||
goTo,
|
||||
goBack,
|
||||
routerState$
|
||||
);
|
||||
})
|
||||
// allow store subscribe to subscribe to actions
|
||||
.delay(10)
|
||||
.flatMap(({ props, appCat }) => {
|
||||
props.history = history;
|
||||
return Render(
|
||||
|
||||
return render$(
|
||||
appCat,
|
||||
React.createElement(Router, props),
|
||||
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',
|
||||
fetchAction: 'appActions.getUser',
|
||||
isPrimed({ username }) {
|
||||
return !!username;
|
||||
},
|
||||
getPayload(props) {
|
||||
return {
|
||||
isPrimed: !!props.username
|
||||
@ -25,22 +28,6 @@ export default contain(
|
||||
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() {
|
||||
const { username, points, picture } = this.props;
|
||||
const navProps = { username, points, picture };
|
||||
|
@ -1,17 +1,65 @@
|
||||
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, HikesStore } from './routes/Hikes/flux';
|
||||
import { JobActions, JobsStore} from './routes/Jobs/flux';
|
||||
import { HikesActions } from './routes/Hikes/flux';
|
||||
import { JobActions } from './routes/Jobs/flux';
|
||||
|
||||
export default Cat()
|
||||
.init(({ instance: cat, args: [services] }) => {
|
||||
cat.register(AppActions, null, services);
|
||||
cat.register(AppStore, null, cat);
|
||||
const ajaxStamp = stamp({
|
||||
methods: {
|
||||
postJSON$,
|
||||
post$
|
||||
}
|
||||
});
|
||||
|
||||
cat.register(HikesActions, null, services);
|
||||
cat.register(HikesStore, null, cat);
|
||||
export default Cat().init(({ instance: cat, args: [services] }) => {
|
||||
const serviceStamp = stamp({
|
||||
methods: {
|
||||
readService$(resource, params, config) {
|
||||
|
||||
cat.register(JobActions, null, cat, services);
|
||||
cat.register(JobsStore, null, cat);
|
||||
return Observable.create(function(observer) {
|
||||
services.read(resource, params, config, (err, res) => {
|
||||
if (err) {
|
||||
return observer.onError(err);
|
||||
}
|
||||
|
||||
observer.onNext(res);
|
||||
observer.onCompleted();
|
||||
});
|
||||
|
||||
return Disposable.create(function() {
|
||||
observer.dispose();
|
||||
});
|
||||
});
|
||||
},
|
||||
createService$(resource, params, body, config) {
|
||||
return Observable.create(function(observer) {
|
||||
services.create(resource, params, body, config, (err, res) => {
|
||||
if (err) {
|
||||
return observer.onError(err);
|
||||
}
|
||||
|
||||
observer.onNext(res);
|
||||
observer.onCompleted();
|
||||
});
|
||||
|
||||
return Disposable.create(function() {
|
||||
observer.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services);
|
||||
cat.register(AppActions.compose(serviceStamp), null, services);
|
||||
cat.register(
|
||||
JobActions.compose(serviceStamp, ajaxStamp),
|
||||
null,
|
||||
cat,
|
||||
services
|
||||
);
|
||||
cat.register(AppStore, null, cat);
|
||||
});
|
||||
|
@ -1,49 +1,45 @@
|
||||
import { Actions } from 'thundercats';
|
||||
import debugFactory from 'debug';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
const debug = debugFactory('freecc:app:actions');
|
||||
|
||||
export default Actions({
|
||||
shouldBindMethods: true,
|
||||
refs: { displayName: 'AppActions' },
|
||||
|
||||
setTitle(title = 'Learn To Code') {
|
||||
return { title: title + '| Free Code Camp' };
|
||||
return { title: title + ' | Free Code Camp' };
|
||||
},
|
||||
|
||||
setUser({
|
||||
username,
|
||||
picture,
|
||||
progressTimestamps = [],
|
||||
isFrontEndCert,
|
||||
isFullStackCert
|
||||
}) {
|
||||
getUser() {
|
||||
return this.readService$('user', null, null)
|
||||
.map(({
|
||||
username,
|
||||
picture,
|
||||
progressTimestamps = [],
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isFullStackCert
|
||||
}) => {
|
||||
return {
|
||||
username,
|
||||
picture,
|
||||
points: progressTimestamps.length,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isFullStackCert
|
||||
};
|
||||
})
|
||||
.catch(err => Observable.just({ err }));
|
||||
},
|
||||
|
||||
// routing
|
||||
goTo: null,
|
||||
goBack: null,
|
||||
updateLocation(location) {
|
||||
return {
|
||||
username,
|
||||
picture,
|
||||
points: progressTimestamps.length,
|
||||
isFrontEndCert,
|
||||
isFullStackCert
|
||||
};
|
||||
},
|
||||
|
||||
getUser: null,
|
||||
updateRoute(route) {
|
||||
return { route };
|
||||
},
|
||||
goBack: null
|
||||
})
|
||||
.refs({ displayName: 'AppActions' })
|
||||
.init(({ instance: appActions, args: [services] }) => {
|
||||
appActions.getUser.subscribe(({ isPrimed }) => {
|
||||
if (isPrimed) {
|
||||
debug('isPrimed');
|
||||
return;
|
||||
transform(state) {
|
||||
return { ...state, location };
|
||||
}
|
||||
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',
|
||||
username: null,
|
||||
picture: null,
|
||||
points: 0
|
||||
points: 0,
|
||||
hikesApp: {
|
||||
hikes: [],
|
||||
// lecture state
|
||||
currentHike: {},
|
||||
showQuestions: false
|
||||
},
|
||||
jobsApp: {
|
||||
showModal: false
|
||||
}
|
||||
};
|
||||
|
||||
export default Store({
|
||||
@ -13,12 +22,82 @@ export default Store({
|
||||
displayName: 'AppStore',
|
||||
value: initValue
|
||||
},
|
||||
init({ instance: appStore, args: [cat] }) {
|
||||
const { updateRoute, setUser, setTitle } = cat.getActions('appActions');
|
||||
const register = createRegistrar(appStore);
|
||||
init({ instance: store, args: [cat] }) {
|
||||
const register = createRegistrar(store);
|
||||
// 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(
|
||||
{
|
||||
store: 'hikesStore',
|
||||
store: 'appStore',
|
||||
map(state) {
|
||||
return state.hikesApp;
|
||||
},
|
||||
actions: ['appActions'],
|
||||
fetchAction: 'hikesActions.fetchHikes',
|
||||
getPayload: ({ hikes, params }) => ({
|
||||
@ -27,7 +30,9 @@ export default contain(
|
||||
appActions: PropTypes.object,
|
||||
children: PropTypes.element,
|
||||
currentHike: PropTypes.object,
|
||||
hikes: PropTypes.array
|
||||
hikes: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
showQuestions: PropTypes.bool
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
@ -41,21 +46,26 @@ export default contain(
|
||||
);
|
||||
},
|
||||
|
||||
renderChild(children, hikes, currentHike) {
|
||||
renderChild({ children, ...props }) {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
return React.cloneElement(children, { hikes, currentHike });
|
||||
return React.cloneElement(children, props);
|
||||
},
|
||||
|
||||
render() {
|
||||
const { hikes, children, currentHike } = this.props;
|
||||
const { hikes } = this.props;
|
||||
const { dashedName } = this.props.params;
|
||||
const preventOverflow = { overflow: 'hidden' };
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,71 +1,88 @@
|
||||
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 Vimeo from 'react-vimeo';
|
||||
import debugFactory from 'debug';
|
||||
|
||||
const debug = debugFactory('freecc:hikes');
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'Lecture',
|
||||
mixins: [History],
|
||||
export default contain(
|
||||
{
|
||||
actions: ['hikesActions'],
|
||||
store: 'appStore',
|
||||
map(state) {
|
||||
const {
|
||||
currentHike: {
|
||||
dashedName,
|
||||
description,
|
||||
challengeSeed: [id] = [0]
|
||||
} = {}
|
||||
} = state.hikesApp;
|
||||
|
||||
propTypes: {
|
||||
currentHike: PropTypes.object,
|
||||
params: PropTypes.object
|
||||
return {
|
||||
dashedName,
|
||||
description,
|
||||
id
|
||||
};
|
||||
}
|
||||
},
|
||||
React.createClass({
|
||||
displayName: 'Lecture',
|
||||
mixins: [History],
|
||||
|
||||
handleError: debug,
|
||||
propTypes: {
|
||||
dashedName: PropTypes.string,
|
||||
description: PropTypes.array,
|
||||
id: PropTypes.string,
|
||||
hikesActions: PropTypes.object
|
||||
},
|
||||
|
||||
handleFinish() {
|
||||
debug('loading questions');
|
||||
const { dashedName } = this.props.params;
|
||||
this.history.pushState(null, `/hikes/${dashedName}/questions/1`);
|
||||
},
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { props } = this;
|
||||
return nextProps.id !== props.id;
|
||||
},
|
||||
|
||||
renderTranscript(transcript, dashedName) {
|
||||
return transcript.map((line, index) => (
|
||||
<p key={ dashedName + index }>{ line }</p>
|
||||
));
|
||||
},
|
||||
handleError: debug,
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
challengeSeed = ['1'],
|
||||
description = []
|
||||
} = this.props.currentHike;
|
||||
const { dashedName } = this.props.params;
|
||||
handleFinish(hikesActions) {
|
||||
debug('loading questions');
|
||||
hikesActions.toggleQuestions();
|
||||
},
|
||||
|
||||
const [ id ] = challengeSeed;
|
||||
renderTranscript(transcript, dashedName) {
|
||||
return transcript.map((line, index) => (
|
||||
<p key={ dashedName + index }>{ line }</p>
|
||||
));
|
||||
},
|
||||
|
||||
const videoTitle = <h2>{ title }</h2>;
|
||||
return (
|
||||
<Col xs={ 12 }>
|
||||
<Row>
|
||||
<Panel className={ 'text-center' } title={ videoTitle }>
|
||||
render() {
|
||||
const {
|
||||
id = '1',
|
||||
description = [],
|
||||
hikesActions
|
||||
} = this.props;
|
||||
const dashedName = 'foo';
|
||||
|
||||
return (
|
||||
<Col xs={ 12 }>
|
||||
<Row>
|
||||
<Vimeo
|
||||
onError={ this.handleError }
|
||||
onFinish= { this.handleFinish }
|
||||
onFinish= { () => this.handleFinish(hikesActions) }
|
||||
videoId={ id } />
|
||||
</Panel>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={ 12 }>
|
||||
<Panel>
|
||||
{ this.renderTranscript(description, dashedName) }
|
||||
</Panel>
|
||||
<Panel>
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
onClick={ this.handleFinish }>
|
||||
Take me to the Questions
|
||||
</Button>
|
||||
</Panel>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
});
|
||||
</Row>
|
||||
<Row>
|
||||
{ this.renderTranscript(description, dashedName) }
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
onClick={ () => this.handleFinish(hikesActions) }>
|
||||
Take me to the Questions
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -11,7 +11,7 @@ export default React.createClass({
|
||||
|
||||
render() {
|
||||
const {
|
||||
hikes
|
||||
hikes = [{}]
|
||||
} = this.props;
|
||||
|
||||
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 debugFactory from 'debug';
|
||||
|
||||
@ -24,41 +26,247 @@ function getCurrentHike(hikes = [{}], dashedName, 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({
|
||||
// start fetching hikes
|
||||
fetchHikes: null,
|
||||
// set hikes on store
|
||||
setHikes: null
|
||||
})
|
||||
.refs({ displayName: 'HikesActions' })
|
||||
.init(({ instance: hikeActions, args: [services] }) => {
|
||||
// set up hikes fetching
|
||||
hikeActions.fetchHikes.subscribe(
|
||||
({ isPrimed, dashedName }) => {
|
||||
if (isPrimed) {
|
||||
return hikeActions.setHikes({
|
||||
transform: (oldState) => {
|
||||
const { hikes } = oldState;
|
||||
const currentHike = getCurrentHike(
|
||||
hikes,
|
||||
dashedName,
|
||||
oldState.currentHike
|
||||
);
|
||||
return Object.assign({}, oldState, { currentHike });
|
||||
}
|
||||
});
|
||||
refs: { displayName: 'HikesActions' },
|
||||
shouldBindMethods: true,
|
||||
fetchHikes({ isPrimed, dashedName }) {
|
||||
if (isPrimed) {
|
||||
return {
|
||||
transform: (state) => {
|
||||
|
||||
const { hikesApp: oldState } = state;
|
||||
const currentHike = getCurrentHike(
|
||||
oldState.hikes,
|
||||
dashedName,
|
||||
oldState.currentHike
|
||||
);
|
||||
|
||||
const hikesApp = { ...oldState, currentHike };
|
||||
return Object.assign({}, state, { hikesApp });
|
||||
}
|
||||
services.read('hikes', null, null, (err, hikes) => {
|
||||
if (err) {
|
||||
debug('an error occurred fetching hikes', err);
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
hikeActions.setHikes({
|
||||
set: {
|
||||
hikes: hikes,
|
||||
currentHike: getCurrentHike(hikes, dashedName)
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
.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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// challenge completed
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return Observable.just({
|
||||
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 HikesStore } from './Store';
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Hikes from './components/Hikes.jsx';
|
||||
import Lecture from './components/Lecture.jsx';
|
||||
import Question from './components/Question.jsx';
|
||||
import Hike from './components/Hike.jsx';
|
||||
|
||||
/*
|
||||
* show video /hikes/someVideo
|
||||
@ -12,9 +11,6 @@ export default {
|
||||
component: Hikes,
|
||||
childRoutes: [{
|
||||
path: ':dashedName',
|
||||
component: Lecture
|
||||
}, {
|
||||
path: ':dashedName/questions/:number',
|
||||
component: Question
|
||||
component: Hike
|
||||
}]
|
||||
};
|
||||
|
@ -11,13 +11,13 @@ const paypalIds = {
|
||||
|
||||
export default contain(
|
||||
{
|
||||
store: 'JobsStore',
|
||||
store: 'appStore',
|
||||
actions: [
|
||||
'jobActions',
|
||||
'appActions'
|
||||
],
|
||||
map({
|
||||
job: { id, isHighlighted } = {},
|
||||
map({ jobsApp: {
|
||||
currentJob: { id, isHighlighted } = {},
|
||||
buttonId = isHighlighted ?
|
||||
paypalIds.highlighted :
|
||||
paypalIds.regular,
|
||||
@ -25,8 +25,8 @@ export default contain(
|
||||
discountAmount = 0,
|
||||
promoCode = '',
|
||||
promoApplied = false,
|
||||
promoName
|
||||
}) {
|
||||
promoName = ''
|
||||
}}) {
|
||||
return {
|
||||
id,
|
||||
isHighlighted,
|
||||
@ -57,7 +57,7 @@ export default contain(
|
||||
|
||||
goToJobBoard() {
|
||||
const { appActions } = this.props;
|
||||
appActions.updateRoute('/jobs');
|
||||
appActions.goTo('/jobs');
|
||||
},
|
||||
|
||||
renderDiscount(discountAmount) {
|
||||
|
@ -6,8 +6,14 @@ import ListJobs from './List.jsx';
|
||||
|
||||
export default contain(
|
||||
{
|
||||
store: 'jobsStore',
|
||||
store: 'appStore',
|
||||
map({ jobsApp: { jobs, showModal }}) {
|
||||
return { jobs, showModal };
|
||||
},
|
||||
fetchAction: 'jobActions.getJobs',
|
||||
isPrimed({ jobs = [] }) {
|
||||
return !!jobs.length;
|
||||
},
|
||||
actions: [
|
||||
'appActions',
|
||||
'jobActions'
|
||||
@ -18,25 +24,19 @@ export default contain(
|
||||
|
||||
propTypes: {
|
||||
children: PropTypes.element,
|
||||
numOfFollowers: PropTypes.number,
|
||||
appActions: PropTypes.object,
|
||||
jobActions: PropTypes.object,
|
||||
jobs: PropTypes.array,
|
||||
showModal: PropTypes.bool
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
const { jobActions } = this.props;
|
||||
jobActions.getFollowers();
|
||||
},
|
||||
|
||||
handleJobClick(id) {
|
||||
const { appActions, jobActions } = this.props;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
jobActions.findJob(id);
|
||||
appActions.updateRoute(`/jobs/${id}`);
|
||||
appActions.goTo(`/jobs/${id}`);
|
||||
},
|
||||
|
||||
renderList(handleJobClick, jobs) {
|
||||
@ -84,7 +84,7 @@ export default contain(
|
||||
<Button
|
||||
className='signup-btn btn-block btn-cta'
|
||||
onClick={ ()=> {
|
||||
appActions.updateRoute('/jobs/new');
|
||||
appActions.goTo('/jobs/new');
|
||||
}}>
|
||||
Post a job: $1,000
|
||||
</Button>
|
||||
|
@ -103,9 +103,9 @@ function makeRequired(validator) {
|
||||
}
|
||||
|
||||
export default contain({
|
||||
store: 'appStore',
|
||||
actions: 'jobActions',
|
||||
store: 'jobsStore',
|
||||
map({ form = {} }) {
|
||||
map({ jobsApp: { form = {} } }) {
|
||||
const {
|
||||
position,
|
||||
locale,
|
||||
@ -115,7 +115,7 @@ export default contain({
|
||||
logo,
|
||||
company,
|
||||
isFrontEndCert = true,
|
||||
isFullStackCert,
|
||||
isBackEndCert,
|
||||
isHighlighted,
|
||||
isRemoteOk,
|
||||
howToApply
|
||||
@ -132,7 +132,7 @@ export default contain({
|
||||
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
||||
howToApply: formatValue(howToApply, makeRequired(isAscii)),
|
||||
isFrontEndCert,
|
||||
isFullStackCert
|
||||
isBackEndCert
|
||||
};
|
||||
},
|
||||
subscribeOnWillMount() {
|
||||
@ -154,7 +154,7 @@ export default contain({
|
||||
isHighlighted: PropTypes.object,
|
||||
isRemoteOk: PropTypes.object,
|
||||
isFrontEndCert: PropTypes.bool,
|
||||
isFullStackCert: PropTypes.bool,
|
||||
isBackEndCert: PropTypes.bool,
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@ -188,7 +192,7 @@ export default contain({
|
||||
logo,
|
||||
company,
|
||||
isFrontEndCert,
|
||||
isFullStackCert,
|
||||
isBackEndCert,
|
||||
isHighlighted,
|
||||
isRemoteOk,
|
||||
howToApply
|
||||
@ -207,7 +211,7 @@ export default contain({
|
||||
isRemoteOk: !!isRemoteOk.value,
|
||||
howToApply: inHTMLData(howToApply.value),
|
||||
isFrontEndCert,
|
||||
isFullStackCert
|
||||
isBackEndCert
|
||||
};
|
||||
|
||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
||||
@ -237,7 +241,7 @@ export default contain({
|
||||
handleCertClick(name) {
|
||||
const { jobActions: { handleForm } } = this.props;
|
||||
const otherButton = name === 'isFrontEndCert' ?
|
||||
'isFullStackCert' :
|
||||
'isBackEndCert' :
|
||||
'isFrontEndCert';
|
||||
|
||||
handleForm({
|
||||
@ -259,7 +263,7 @@ export default contain({
|
||||
isRemoteOk,
|
||||
howToApply,
|
||||
isFrontEndCert,
|
||||
isFullStackCert,
|
||||
isBackEndCert,
|
||||
jobActions: { handleForm }
|
||||
} = this.props;
|
||||
|
||||
@ -306,13 +310,13 @@ export default contain({
|
||||
<div className='button-spacer' />
|
||||
<Row>
|
||||
<Button
|
||||
className={ isFullStackCert ? 'active' : ''}
|
||||
className={ isBackEndCert ? 'active' : ''}
|
||||
onClick={ () => {
|
||||
if (!isFullStackCert) {
|
||||
this.handleCertClick('isFullStackCert');
|
||||
if (!isBackEndCert) {
|
||||
this.handleCertClick('isBackEndCert');
|
||||
}
|
||||
}}>
|
||||
<h4>Full Stack Development Certified</h4>
|
||||
<h4>Back End Development Certified</h4>
|
||||
You can expect each applicant to have a code
|
||||
portfolio using the following technologies:
|
||||
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
||||
|
@ -8,12 +8,12 @@ import JobNotFound from './JobNotFound.jsx';
|
||||
|
||||
export default contain(
|
||||
{
|
||||
store: 'JobsStore',
|
||||
store: 'appStore',
|
||||
actions: [
|
||||
'appActions',
|
||||
'jobActions'
|
||||
],
|
||||
map({ form: job = {} }) {
|
||||
map({ jobsApp: { form: job = {} } }) {
|
||||
return { job };
|
||||
}
|
||||
},
|
||||
@ -32,7 +32,7 @@ export default contain(
|
||||
const { appActions, job } = this.props;
|
||||
// redirect user in client
|
||||
if (!job || !job.position || !job.description) {
|
||||
appActions.updateRoute('/jobs/new');
|
||||
appActions.goTo('/jobs/new');
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -9,25 +9,25 @@ import { isJobValid } from '../utils';
|
||||
function shouldShowApply(
|
||||
{
|
||||
isFrontEndCert: isFrontEndCertReq = false,
|
||||
isFullStackCert: isFullStackCertReq = false
|
||||
isBackEndCert: isBackEndCertReq = false
|
||||
}, {
|
||||
isFrontEndCert = false,
|
||||
isFullStackCert = false
|
||||
isBackEndCert = false
|
||||
}
|
||||
) {
|
||||
return (!isFrontEndCertReq && !isFullStackCertReq) ||
|
||||
(isFullStackCertReq && isFullStackCert) ||
|
||||
return (!isFrontEndCertReq && !isBackEndCertReq) ||
|
||||
(isBackEndCertReq && isBackEndCert) ||
|
||||
(isFrontEndCertReq && isFrontEndCert);
|
||||
}
|
||||
|
||||
function generateMessage(
|
||||
{
|
||||
isFrontEndCert: isFrontEndCertReq = false,
|
||||
isFullStackCert: isFullStackCertReq = false
|
||||
isBackEndCert: isBackEndCertReq = false
|
||||
},
|
||||
{
|
||||
isFrontEndCert = false,
|
||||
isFullStackCert = false,
|
||||
isBackEndCert = false,
|
||||
isSignedIn = false
|
||||
}
|
||||
) {
|
||||
@ -39,39 +39,40 @@ function generateMessage(
|
||||
return 'This employer requires Free Code Camp’s Front ' +
|
||||
'End Development Certification in order to apply';
|
||||
}
|
||||
if (isFullStackCertReq && !isFullStackCert) {
|
||||
return 'This employer requires Free Code Camp’s Full ' +
|
||||
'Stack Development Certification in order to apply';
|
||||
if (isBackEndCertReq && !isBackEndCert) {
|
||||
return 'This employer requires Free Code Camp’s Back ' +
|
||||
'End Development Certification in order to apply';
|
||||
}
|
||||
if (isFrontEndCertReq && isFrontEndCertReq) {
|
||||
return 'This employer requires the Front End Development Certification. ' +
|
||||
"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.";
|
||||
}
|
||||
|
||||
export default contain(
|
||||
{
|
||||
stores: ['appStore', 'jobsStore'],
|
||||
fetchWaitFor: 'jobsStore',
|
||||
store: 'appStore',
|
||||
fetchAction: 'jobActions.getJob',
|
||||
combineLatest(
|
||||
{ username, isFrontEndCert, isFullStackCert },
|
||||
{ currentJob }
|
||||
) {
|
||||
map({
|
||||
username,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
jobsApp: { currentJob }
|
||||
}) {
|
||||
return {
|
||||
username,
|
||||
job: currentJob,
|
||||
isFrontEndCert,
|
||||
isFullStackCert
|
||||
isBackEndCert
|
||||
};
|
||||
},
|
||||
getPayload({ params: { id }, job = {} }) {
|
||||
return {
|
||||
id,
|
||||
isPrimed: job.id === id
|
||||
};
|
||||
getPayload({ params: { id } }) {
|
||||
return id;
|
||||
},
|
||||
isPrimed({ params: { id } = {}, job = {} }) {
|
||||
return job.id === id;
|
||||
},
|
||||
// using es6 destructuring
|
||||
shouldContainerFetch({ job = {} }, { params: { id } }
|
||||
@ -84,7 +85,7 @@ export default contain(
|
||||
|
||||
propTypes: {
|
||||
job: PropTypes.object,
|
||||
isFullStackCert: PropTypes.bool,
|
||||
isBackEndCert: PropTypes.bool,
|
||||
isFrontEndCert: PropTypes.bool,
|
||||
username: PropTypes.string
|
||||
},
|
||||
@ -101,7 +102,7 @@ export default contain(
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFullStackCert,
|
||||
isBackEndCert,
|
||||
isFrontEndCert,
|
||||
job,
|
||||
username
|
||||
@ -115,12 +116,12 @@ export default contain(
|
||||
|
||||
const showApply = shouldShowApply(
|
||||
job,
|
||||
{ isFrontEndCert, isFullStackCert }
|
||||
{ isFrontEndCert, isBackEndCert }
|
||||
);
|
||||
|
||||
const message = generateMessage(
|
||||
job,
|
||||
{ isFrontEndCert, isFullStackCert, isSignedIn }
|
||||
{ isFrontEndCert, isBackEndCert, isSignedIn }
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,51 +1,102 @@
|
||||
import { Actions } from 'thundercats';
|
||||
import store from 'store';
|
||||
import debugFactory from 'debug';
|
||||
import { jsonp$ } from '../../../../utils/jsonp$';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { nameSpacedTransformer } from '../../../../utils';
|
||||
|
||||
const debug = debugFactory('freecc:jobs:actions');
|
||||
const assign = Object.assign;
|
||||
const jobsTranformer = nameSpacedTransformer('jobsApp');
|
||||
const noOper = { transform: () => {} };
|
||||
|
||||
export default Actions({
|
||||
setJobs: null,
|
||||
refs: { displayName: 'JobActions' },
|
||||
shouldBindMethods: true,
|
||||
// findJob assumes that the job is already in the list of jobs
|
||||
findJob(id) {
|
||||
return oldState => {
|
||||
const { currentJob = {}, jobs = [] } = oldState;
|
||||
// currentJob already set
|
||||
// do nothing
|
||||
if (currentJob.id === id) {
|
||||
return null;
|
||||
}
|
||||
const foundJob = jobs.reduce((newJob, job) => {
|
||||
if (job.id === id) {
|
||||
return job;
|
||||
return {
|
||||
transform: jobsTranformer(oldState => {
|
||||
const { currentJob = {}, jobs = [] } = oldState;
|
||||
// currentJob already set
|
||||
// do nothing
|
||||
if (currentJob.id === id) {
|
||||
return null;
|
||||
}
|
||||
return newJob;
|
||||
}, null);
|
||||
const foundJob = jobs.reduce((newJob, job) => {
|
||||
if (job.id === id) {
|
||||
return job;
|
||||
}
|
||||
return newJob;
|
||||
}, null);
|
||||
|
||||
// if no job found this will be null which is a op noop
|
||||
return foundJob ?
|
||||
assign({}, oldState, { currentJob: foundJob }) :
|
||||
null;
|
||||
// if no job found this will be null which is a op noop
|
||||
return foundJob ?
|
||||
assign({}, oldState, { currentJob: foundJob }) :
|
||||
null;
|
||||
})
|
||||
};
|
||||
},
|
||||
setError: null,
|
||||
getJob: null,
|
||||
saveJobToDb: null,
|
||||
getJobs(params) {
|
||||
return { params };
|
||||
saveJobToDb({ goTo, job }) {
|
||||
return this.createService$('jobs', { job })
|
||||
.map(job => ({
|
||||
transform(state) {
|
||||
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() {
|
||||
return { showModal: true };
|
||||
return {
|
||||
transform: jobsTranformer(state => ({ ...state, showModal: true }))
|
||||
};
|
||||
},
|
||||
closeModal() {
|
||||
return { showModal: false };
|
||||
return {
|
||||
transform: jobsTranformer(state => ({ ...state, showModal: false }))
|
||||
};
|
||||
},
|
||||
handleForm(value) {
|
||||
return {
|
||||
transform(oldState) {
|
||||
transform: jobsTranformer(oldState => {
|
||||
const { form } = oldState;
|
||||
const newState = assign({}, oldState);
|
||||
newState.form = assign(
|
||||
@ -54,142 +105,94 @@ export default Actions({
|
||||
value
|
||||
);
|
||||
return newState;
|
||||
}
|
||||
})
|
||||
};
|
||||
},
|
||||
saveForm: null,
|
||||
getSavedForm: null,
|
||||
clearSavedForm: null,
|
||||
setForm(form) {
|
||||
return { form };
|
||||
},
|
||||
getFollowers: null,
|
||||
setFollowersCount(numOfFollowers) {
|
||||
return { numOfFollowers };
|
||||
getSavedForm() {
|
||||
const form = store.get('newJob');
|
||||
if (form && !Array.isArray(form) && typeof form === 'object') {
|
||||
return {
|
||||
transform: jobsTranformer(state => {
|
||||
return { ...state, form };
|
||||
})
|
||||
};
|
||||
}
|
||||
return noOper;
|
||||
},
|
||||
setPromoCode({ target: { value = '' }} = {}) {
|
||||
return { promoCode: value.replace(/[^\d\w\s]/, '') };
|
||||
},
|
||||
applyCode: null,
|
||||
clearPromo(foo, undef) {
|
||||
return {
|
||||
price: undef,
|
||||
buttonId: undef,
|
||||
discountAmount: undef,
|
||||
promoCode: undef,
|
||||
promoApplied: false,
|
||||
promoName: undef
|
||||
transform: jobsTranformer(state => ({
|
||||
...state,
|
||||
promoCode: value.replace(/[^\d\w\s]/, '')
|
||||
}))
|
||||
};
|
||||
},
|
||||
applyPromo({
|
||||
fullPrice: price,
|
||||
buttonId,
|
||||
discountAmount,
|
||||
code: promoCode,
|
||||
name: promoName
|
||||
} = {}) {
|
||||
return {
|
||||
price,
|
||||
buttonId,
|
||||
discountAmount,
|
||||
promoCode,
|
||||
promoApplied: true,
|
||||
promoName
|
||||
applyCode({ id, code = '', type = null}) {
|
||||
const body = {
|
||||
id,
|
||||
code: code.replace(/[^\d\w\s]/, '')
|
||||
};
|
||||
}
|
||||
})
|
||||
.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 });
|
||||
if (type) {
|
||||
body.type = type;
|
||||
}
|
||||
return this.postJSON$('/api/promos/getButton', body)
|
||||
.pluck('response')
|
||||
.map(({ promo }) => {
|
||||
if (!promo || !promo.buttonId) {
|
||||
return noOper;
|
||||
}
|
||||
jobActions.setJobs({ jobs });
|
||||
});
|
||||
});
|
||||
const {
|
||||
fullPrice: price,
|
||||
buttonId,
|
||||
discountAmount,
|
||||
code: promoCode,
|
||||
name: promoName
|
||||
} = promo;
|
||||
|
||||
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 });
|
||||
return {
|
||||
transform: jobsTranformer(state => ({
|
||||
...state,
|
||||
price,
|
||||
buttonId,
|
||||
discountAmount,
|
||||
promoCode,
|
||||
promoApplied: true,
|
||||
promoName
|
||||
}))
|
||||
};
|
||||
})
|
||||
.catch(err => Observable.just({
|
||||
transform(state) {
|
||||
return { ...state, err };
|
||||
}
|
||||
if (job) {
|
||||
jobActions.setJobs({ currentJob: job });
|
||||
}
|
||||
jobActions.setJobs({});
|
||||
});
|
||||
});
|
||||
|
||||
}));
|
||||
},
|
||||
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.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 = {
|
||||
id,
|
||||
code: code.replace(/[^\d\w\s]/, '')
|
||||
};
|
||||
if (type) {
|
||||
body.type = type;
|
||||
}
|
||||
postJSON$('/api/promos/getButton', body)
|
||||
.pluck('response')
|
||||
.subscribe(
|
||||
({ promo }) => {
|
||||
if (promo && promo.buttonId) {
|
||||
jobActions.applyPromo(promo);
|
||||
}
|
||||
jobActions.setError(new Error('no promo found'));
|
||||
},
|
||||
jobActions.setError
|
||||
);
|
||||
});
|
||||
|
||||
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 JobsStore } from './Store';
|
||||
|
@ -74,6 +74,11 @@
|
||||
"defaut": false,
|
||||
"description": "Camper must be front end certified to apply"
|
||||
},
|
||||
"isBackEndCert": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Camper must be back end certified to apply"
|
||||
},
|
||||
"isFullStackCert": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
import debugFactory from 'debug';
|
||||
import { AnonymousObservable, helpers } from 'rx';
|
||||
import { Observable, AnonymousObservable, helpers } from 'rx';
|
||||
|
||||
const debug = debugFactory('freecc:ajax$');
|
||||
const root = typeof window !== 'undefined' ? window : {};
|
||||
@ -147,8 +147,12 @@ export function ajax$(options) {
|
||||
var processResponse = function(xhr, e) {
|
||||
var status = xhr.status === 1223 ? 204 : xhr.status;
|
||||
if ((status >= 200 && status <= 300) || status === 0 || status === '') {
|
||||
observer.onNext(normalizeSuccess(e, xhr, settings));
|
||||
observer.onCompleted();
|
||||
try {
|
||||
observer.onNext(normalizeSuccess(e, xhr, settings));
|
||||
observer.onCompleted();
|
||||
} catch (err) {
|
||||
observer.onError(err);
|
||||
}
|
||||
} else {
|
||||
observer.onError(normalizeError(e, xhr, 'error'));
|
||||
}
|
||||
@ -228,8 +232,8 @@ export function ajax$(options) {
|
||||
settings.hasContent && settings.body
|
||||
);
|
||||
xhr.send(settings.hasContent && settings.body || null);
|
||||
} catch (e) {
|
||||
observer.onError(e);
|
||||
} catch (err) {
|
||||
observer.onError(err);
|
||||
}
|
||||
|
||||
return function() {
|
||||
@ -247,13 +251,25 @@ export function ajax$(options) {
|
||||
* from the Ajax POST.
|
||||
*/
|
||||
export function post$(url, body) {
|
||||
try {
|
||||
body = JSON.stringify(body);
|
||||
} catch (e) {
|
||||
return Observable.throw(e);
|
||||
}
|
||||
|
||||
return ajax$({ url, body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function postJSON$(url, body) {
|
||||
try {
|
||||
body = JSON.stringify(body);
|
||||
} catch (e) {
|
||||
return Observable.throw(e);
|
||||
}
|
||||
|
||||
return ajax$({
|
||||
url,
|
||||
body: JSON.stringify(body),
|
||||
body,
|
||||
method: 'POST',
|
||||
responseType: '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 };
|
||||
};
|
||||
}
|
13
gulpfile.js
13
gulpfile.js
@ -40,7 +40,7 @@ var Rx = require('rx'),
|
||||
// lint
|
||||
jsonlint = require('gulp-jsonlint'),
|
||||
eslint = require('gulp-eslint'),
|
||||
|
||||
|
||||
// unit-tests
|
||||
tape = require('gulp-tape'),
|
||||
tapSpec = require('tap-spec');
|
||||
@ -183,7 +183,7 @@ function delRev(dest, manifestName) {
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task('serve', function(cb) {
|
||||
gulp.task('serve', ['build-manifest'], function(cb) {
|
||||
var called = false;
|
||||
nodemon({
|
||||
script: paths.server,
|
||||
@ -483,6 +483,10 @@ function buildManifest() {
|
||||
|
||||
var buildDependents = ['less', 'js', 'dependents'];
|
||||
|
||||
if (__DEV__) {
|
||||
buildDependents.push('pack-watch-manifest');
|
||||
}
|
||||
|
||||
gulp.task('build-manifest', buildDependents, function() {
|
||||
return buildManifest();
|
||||
});
|
||||
@ -505,9 +509,9 @@ var watchDependents = [
|
||||
'dependents',
|
||||
'serve',
|
||||
'sync',
|
||||
'build-manifest',
|
||||
'pack-watch',
|
||||
'pack-watch-manifest'
|
||||
'pack-watch-manifest',
|
||||
'build-manifest'
|
||||
];
|
||||
|
||||
gulp.task('reload', function() {
|
||||
@ -533,6 +537,7 @@ gulp.task('default', [
|
||||
'serve',
|
||||
'pack-watch',
|
||||
'pack-watch-manifest',
|
||||
'build-manifest-watch',
|
||||
'watch',
|
||||
'sync'
|
||||
]);
|
||||
|
@ -115,9 +115,10 @@
|
||||
"rx": "^4.0.0",
|
||||
"sanitize-html": "^1.11.1",
|
||||
"sort-keys": "^1.1.1",
|
||||
"stampit": "^2.1.1",
|
||||
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
||||
"thundercats": "^3.0.0",
|
||||
"thundercats-react": "~0.4.0",
|
||||
"thundercats": "^3.1.0",
|
||||
"thundercats-react": "~0.5.1",
|
||||
"twit": "^2.1.1",
|
||||
"uglify-js": "^2.5.0",
|
||||
"url-regex": "^3.0.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Computer Basics",
|
||||
"order": 0.050,
|
||||
"order": 0,
|
||||
"time": "3h",
|
||||
"challenges": [
|
||||
{
|
||||
|
@ -3,8 +3,10 @@ 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 { app$ } from '../../common/app';
|
||||
import { RenderToString } from 'thundercats-react';
|
||||
|
||||
const debug = debugFactory('freecc:react-server');
|
||||
|
||||
@ -12,14 +14,13 @@ const debug = debugFactory('freecc:react-server');
|
||||
// remove their individual controllers
|
||||
const routes = [
|
||||
'/jobs',
|
||||
'/jobs/*'
|
||||
];
|
||||
|
||||
const devRoutes = [
|
||||
'/jobs/*',
|
||||
'/hikes',
|
||||
'/hikes/*'
|
||||
];
|
||||
|
||||
const devRoutes = [];
|
||||
|
||||
export default function reactSubRouter(app) {
|
||||
var router = app.loopback.Router();
|
||||
|
||||
@ -51,20 +52,28 @@ export default function reactSubRouter(app) {
|
||||
return !!props;
|
||||
})
|
||||
.flatMap(function({ props, AppCat }) {
|
||||
// call thundercats renderToString
|
||||
// prefetches data and sets up it up for current state
|
||||
debug('rendering to string');
|
||||
return RenderToString(
|
||||
AppCat(null, services),
|
||||
const cat = AppCat(null, services);
|
||||
debug('render react markup and pre-fetch data');
|
||||
const store = cat.getStore('appStore');
|
||||
|
||||
// primes store to observe action changes
|
||||
// cleaned up by cat.dispose further down
|
||||
store.subscribe(() => {});
|
||||
|
||||
return renderToString$(
|
||||
cat,
|
||||
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 }) {
|
||||
debug('react rendered');
|
||||
.flatMap(function({ data, markup, cat }) {
|
||||
debug('react markup rendered, data fetched');
|
||||
cat.dispose();
|
||||
const { title } = data.AppStore;
|
||||
res.expose(data, 'data');
|
||||
// now render jade file with markup injected from react
|
||||
return res.render$(
|
||||
'layout-react',
|
||||
{ markup, title }
|
||||
@ -72,7 +81,7 @@ export default function reactSubRouter(app) {
|
||||
})
|
||||
.subscribe(
|
||||
function(markup) {
|
||||
debug('jade rendered');
|
||||
debug('html rendered and ready to send');
|
||||
res.send(markup);
|
||||
},
|
||||
next
|
||||
|
@ -1,9 +1,13 @@
|
||||
import helmet from 'helmet';
|
||||
|
||||
const trusted = [
|
||||
let trusted = [
|
||||
"'self'"
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
trusted.push('ws://localhost:3001');
|
||||
}
|
||||
|
||||
export default function csp() {
|
||||
return helmet.csp({
|
||||
defaultSrc: trusted,
|
||||
|
@ -11,7 +11,7 @@ export default function hikesService(app) {
|
||||
read: (req, resource, params, config, cb) => {
|
||||
const query = {
|
||||
where: { challengeType: '6' },
|
||||
order: 'suborder ASC'
|
||||
order: ['order ASC', 'suborder ASC' ]
|
||||
};
|
||||
|
||||
debug('params', params);
|
||||
|
Reference in New Issue
Block a user