diff --git a/common/app/Cat.js b/common/app/Cat.js
index fb442e3db6..2953c8aa5f 100644
--- a/common/app/Cat.js
+++ b/common/app/Cat.js
@@ -2,10 +2,17 @@ import { Cat } from 'thundercats';
import stamp from 'stampit';
import { Disposable, Observable } from 'rx';
+import { postJSON$ } from '../utils/ajax-stream.js';
import { AppActions, AppStore } from './flux';
import { HikesActions } from './routes/Hikes/flux';
import { JobActions, JobsStore} from './routes/Jobs/flux';
+const ajaxStamp = stamp({
+ methods: {
+ postJSON$: postJSON$
+ }
+});
+
export default Cat().init(({ instance: cat, args: [services] }) => {
const serviceStamp = stamp({
methods: {
@@ -30,7 +37,7 @@ export default Cat().init(({ instance: cat, args: [services] }) => {
}
});
- cat.register(HikesActions.compose(serviceStamp), null, services);
+ cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services);
cat.register(AppActions.compose(serviceStamp), null, services);
cat.register(AppStore, null, cat);
diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js
index 59f2f7e959..b052365a92 100644
--- a/common/app/flux/Store.js
+++ b/common/app/flux/Store.js
@@ -8,7 +8,9 @@ const initValue = {
points: 0,
hikesApp: {
hikes: [],
- currentHikes: {}
+ currentHikes: {},
+ currentQuestion: 1,
+ showQuestion: false
}
};
@@ -20,13 +22,13 @@ export default Store({
init({ instance: appStore, args: [cat] }) {
const { updateRoute, getUser, setTitle } = cat.getActions('appActions');
const register = createRegistrar(appStore);
- const { fetchHikes } = cat.getActions('hikesActions');
+ const { toggleQuestions, fetchHikes } = cat.getActions('hikesActions');
// app
register(setter(fromMany(getUser, setTitle, updateRoute)));
// hikes
- register(fetchHikes);
+ register(fromMany(fetchHikes, toggleQuestions));
return appStore;
}
diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/Hikes/components/Hike.jsx
index de5367ed09..7edb724a92 100644
--- a/common/app/routes/Hikes/components/Hike.jsx
+++ b/common/app/routes/Hikes/components/Hike.jsx
@@ -12,38 +12,22 @@ export default React.createClass({
displayName: 'Hike',
propTypes: {
- dashedName: PropTypes.string,
currentHike: PropTypes.object,
showQuestions: PropTypes.bool
},
- renderBody(showQuestions, currentHike, dashedName) {
+ renderBody(showQuestions) {
if (showQuestions) {
- return (
-
- );
+ return ;
}
-
- const {
- challengeSeed: [ id ] = ['1'],
- description = []
- } = currentHike;
-
- return (
-
- );
+ return ;
},
render() {
const {
- currentHike = {},
- dashedName,
+ currentHike: { title } = {},
showQuestions
} = this.props;
- const { title } = currentHike;
const videoTitle =
{ title }
;
@@ -53,7 +37,7 @@ export default React.createClass({
- { this.renderBody(showQuestions, currentHike, dashedName) }
+ { this.renderBody(showQuestions) }
diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx
index 33cf820666..95aef149fd 100644
--- a/common/app/routes/Hikes/components/Hikes.jsx
+++ b/common/app/routes/Hikes/components/Hikes.jsx
@@ -31,7 +31,8 @@ export default contain(
children: PropTypes.element,
currentHike: PropTypes.object,
hikes: PropTypes.array,
- params: PropTypes.object
+ params: PropTypes.object,
+ showQuestions: PropTypes.bool
},
componentWillMount() {
@@ -45,15 +46,15 @@ export default contain(
);
},
- renderChild(children, hikes, currentHike, dashedName) {
+ renderChild({ children, ...props }) {
if (!children) {
return null;
}
- return React.cloneElement(children, { hikes, currentHike, dashedName });
+ 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 (
@@ -61,7 +62,7 @@ export default contain(
{
// render sub-route
- this.renderChild(children, hikes, currentHike, dashedName) ||
+ this.renderChild({ ...this.props, dashedName }) ||
// if no sub-route render hikes map
this.renderMap(hikes)
}
diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx
index 86ab935c6f..be26aa540a 100644
--- a/common/app/routes/Hikes/components/Lecture.jsx
+++ b/common/app/routes/Hikes/components/Lecture.jsx
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react';
+import { contain } from 'thundercats-react';
import { Button, Col, Row } from 'react-bootstrap';
import { History } from 'react-router';
import Vimeo from 'react-vimeo';
@@ -6,53 +7,82 @@ 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: {
- dashedName: PropTypes.string,
- description: PropTypes.array,
- id: PropTypes.string
+ 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');
- },
+ shouldComponentUpdate(nextProps) {
+ const { props } = this;
+ return nextProps.id !== props.id;
+ },
- renderTranscript(transcript, dashedName) {
- return transcript.map((line, index) => (
- { line }
- ));
- },
+ handleError: debug,
- render() {
- const {
- id = '1',
- dashedName,
- description = []
- } = this.props;
+ handleFinish(hikesActions) {
+ debug('loading questions');
+ hikesActions.toggleQuestions();
+ },
- return (
-
-
-
-
-
- { this.renderTranscript(description, dashedName) }
-
-
-
- );
- }
-});
+ renderTranscript(transcript, dashedName) {
+ return transcript.map((line, index) => (
+ { line }
+ ));
+ },
+
+ render() {
+ const {
+ id = '1',
+ description = [],
+ hikesActions
+ } = this.props;
+ const dashedName = 'foo';
+
+ return (
+
+
+
+
+
+ { this.renderTranscript(description, dashedName) }
+
+
+
+ );
+ }
+ })
+);
diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx
index d426b14916..1a3160ef1e 100644
--- a/common/app/routes/Hikes/components/Questions.jsx
+++ b/common/app/routes/Hikes/components/Questions.jsx
@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { Motion } from 'react-motion';
-import { History, Lifecycle } from 'react-router';
+import { contain } from 'thundercats-react';
import debugFactory from 'debug';
import {
Button,
@@ -10,269 +10,250 @@ import {
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: 'Questions',
+export default contain(
+ {
+ store: 'appStore',
+ actions: ['hikesAction'],
+ map(state) {
+ const { currentQuestion, currentHike } = state.hikesApp;
- 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]);
+ return {
+ hike: currentHike,
+ currentQuestion
+ };
}
- 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]
- });
},
+ React.createClass({
+ displayName: 'Questions',
- handleMouseUp() {
- const { correct } = this.state;
- if (correct) {
- return this.setState({
- isPressed: false,
- delta: [0, 0]
- });
- }
- this.setState({
- isPressed: false,
+ propTypes: {
+ dashedName: PropTypes.string,
+ currentQuestion: PropTypes.number,
+ hike: PropTypes.object,
+ hikesActions: PropTypes.object
+ },
+
+ getInitialState: () => ({
mouse: [0, 0],
- delta: [0, 0]
- });
- },
+ correct: false,
+ delta: [0, 0],
+ isPressed: false,
+ showInfo: false,
+ shake: false
+ }),
- handleMouseMove(answer) {
- return (e) => {
- let { pageX, pageY, touches } = e;
+ getTweenValues() {
+ const { mouse: [x, y] } = this.state;
+ return {
+ val: { x, y },
+ config: [120, 10]
+ };
+ },
+ handleMouseDown({ pageX, pageY, touches }) {
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');
+ const { mouse: [pressX, pressY] } = this.state;
+ const dx = pageX - pressX;
+ const dy = pageY - pressY;
this.setState({
- showInfo: true,
- shake: true
+ isPressed: true,
+ delta: [dx, dy],
+ mouse: [pageX - dx, pageY - dy]
});
+ },
- 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 }`
- );
+ handleMouseUp() {
+ const { correct } = this.state;
+ if (correct) {
+ return this.setState({
+ isPressed: false,
+ delta: [0, 0]
+ });
}
- // 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;
+ 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)();
}
- 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 (
-
-
-
- { info }
-
-
-
-
-
-
- );
- },
-
- 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)`
+ if (mouse[0] <= -ANSWER_THRESHOLD) {
+ this.handleMouseUp();
+ return this.onAnswer(answer, false)();
+ }
+ this.setState({ mouse });
+ }
};
- const title = Question { number }
;
+ },
+
+ 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 {
+ hikesActions,
+ hike: { id, name }
+ } = this.props;
+
+ hikesActions.completedHike({ id, name });
+ },
+
+ 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 (
-
- { question }
-
+
+
+
+ { info }
+
+
+
+
+
+
);
- };
- },
+ },
- 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 (
-
-
-
- { this.renderQuestion(number, question, answer, shake) }
-
- { this.renderInfo(showInfo, info) }
-
-
-
+ 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 = Question { number }
;
+ return (
+
+ { question }
-
-
- );
- }
-});
+ );
+ };
+ },
+
+ render() {
+ const { showInfo, shake } = this.state;
+ const {
+ hike: { tests = [] } = {},
+ currentQuestion
+ } = this.props;
+
+ const [ question, answer, info ] = tests[currentQuestion - 1] || [];
+
+ return (
+
+
+
+ { this.renderQuestion(currentQuestion, question, answer, shake) }
+
+ { this.renderInfo(showInfo, info) }
+
+
+
+
+
+
+ );
+ }
+ })
+);
diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js
index 3f5e262f31..56fe187350 100644
--- a/common/app/routes/Hikes/flux/Actions.js
+++ b/common/app/routes/Hikes/flux/Actions.js
@@ -1,3 +1,5 @@
+import _ from 'lodash';
+import { Observable } from 'rx';
import { Actions } from 'thundercats';
import debugFactory from 'debug';
@@ -24,6 +26,15 @@ 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];
+}
+
export default Actions({
refs: { displayName: 'HikesActions' },
shouldBindMethods: true,
@@ -47,19 +58,53 @@ export default Actions({
return this.readService$('hikes', null, null)
.map(hikes => {
- const hikesApp = {
- hikes,
- currentHike: getCurrentHike(hikes, dashedName)
- };
-
+ const currentHike = getCurrentHike(hikes, dashedName);
return {
- transform(oldState) {
- return Object.assign({}, oldState, { hikesApp });
+ transform(state) {
+ const hikesApp = { ...state.hikesApp, currentHike, hikes };
+ return { ...state, hikesApp };
}
};
})
.catch(err => {
console.error(err);
});
+ },
+
+ toggleQuestions() {
+ return {
+ transform(state) {
+ state.hikesApp.showQuestions = !state.hikesApp.showQuestions;
+ return Object.assign({}, state);
+ }
+ };
+ },
+
+ completedHike(data = {}) {
+ return this.postJSON$('/completed-challenge', data)
+ .map(() => {
+ return {
+ transform(state) {
+ const { hikes, currentHike: { id } } = state.hikesApp;
+ const currentHike = findNextHike(hikes, id);
+
+ // go to next route
+ state.route = currentHike && currentHike.dashedName ?
+ `/hikes/${ currentHike.dashedName }` :
+ '/hikes';
+
+ const hikesApp = { ...state.hikesApp, currentHike };
+ return { ...state, hikesApp };
+ }
+ };
+ })
+ .catch(err => {
+ console.error(err);
+ return Observable.just({
+ set: {
+ error: err
+ }
+ });
+ });
}
});