diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index bad9d098193..46011285a78 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -2,6 +2,7 @@ import React, { PropTypes } from 'react'; import { spring, Motion } from 'react-motion'; import { connect } from 'react-redux'; import { Button, Col, Row } from 'react-bootstrap'; +import { CompositeDisposable } from 'rx'; import { createSelector } from 'reselect'; import { @@ -53,6 +54,11 @@ const mapStateToProps = createSelector( ); class Question extends React.Component { + constructor(...args) { + super(...args); + this._subscriptions = new CompositeDisposable(); + } + static displayName = 'Questions'; static propTypes = { @@ -72,6 +78,10 @@ class Question extends React.Component { shouldShakeQuestion: PropTypes.bool }; + componentWillUnmount() { + this._subscriptions.dispose(); + } + handleMouseUp(e, answer, info) { e.stopPropagation(); if (!this.props.isPressed) { @@ -84,12 +94,15 @@ class Question extends React.Component { } = this.props; releaseQuestion(); - answerQuestion({ + const subscription = answerQuestion({ e, answer, info, threshold: answerThreshold - }); + }) + .subscribe(); + + this._subscriptions.add(subscription); } handleMouseMove(isPressed, { delta, moveQuestion }) { @@ -101,17 +114,21 @@ class Question extends React.Component { onAnswer(answer, userAnswer, info) { const { isSignedIn, answerQuestion } = this.props; + const subscriptions = this._subscriptions; return e => { if (e && e.preventDefault) { e.preventDefault(); } - return answerQuestion({ + const subscription = answerQuestion({ answer, userAnswer, info, isSignedIn - }); + }) + .subscribe(); + + subscriptions.add(subscription); }; } diff --git a/common/app/routes/Hikes/redux/answer-saga.js b/common/app/routes/Hikes/redux/answer-saga.js index c9c25008995..450814adfc5 100644 --- a/common/app/routes/Hikes/redux/answer-saga.js +++ b/common/app/routes/Hikes/redux/answer-saga.js @@ -1,5 +1,5 @@ import { Observable } from 'rx'; -// import { routeActions } from 'react-simple-router'; +import { push } from 'react-router-redux'; import types from './types'; import { getMouse } from './utils'; @@ -7,119 +7,138 @@ import { getMouse } from './utils'; import { makeToast, updatePoints } from '../../../redux/actions'; import { hikeCompleted, goToNextHike } from './actions'; import { postJSON$ } from '../../../../utils/ajax-stream'; +import { getCurrentHike } from './selectors'; -export default () => ({ getState, dispatch }) => next => { - return function answerSaga(action) { - if (types.answerQuestion !== action.type) { +function handleAnswer(getState, dispatch, next, action) { + const { + e, + answer, + userAnswer, + info, + threshold + } = action.payload; + + const state = getState(); + const { id, name, challengeType, tests } = getCurrentHike(state); + const { + app: { isSignedIn }, + hikesApp: { + currentQuestion, + delta = [ 0, 0 ] + } + } = state; + + let finalAnswer; + // drag answer, compute response + if (typeof userAnswer === 'undefined') { + const [positionX] = getMouse(e, delta); + + // question released under threshold + if (Math.abs(positionX) < threshold) { return next(action); } - const { - e, - answer, - userAnswer, - info, - threshold - } = action.payload; - - const { - app: { isSignedIn }, - hikesApp: { - currentQuestion, - currentHike: { id, name, challengeType }, - tests = [], - delta = [ 0, 0 ] - } - } = getState(); - - let finalAnswer; - // drag answer, compute response - if (typeof userAnswer === 'undefined') { - const [positionX] = getMouse(e, delta); - - // question released under threshold - if (Math.abs(positionX) < threshold) { - return next(action); - } - - if (positionX >= threshold) { - finalAnswer = true; - } - - if (positionX <= -threshold) { - finalAnswer = false; - } - } else { - finalAnswer = userAnswer; + if (positionX >= threshold) { + finalAnswer = true; } - // incorrect question - if (answer !== finalAnswer) { - if (info) { - dispatch(makeToast({ - title: 'Hint', - message: info, - type: 'info' - })); - } - - return Observable - .just({ type: types.removeShake }) - .delay(500) - .startWith({ type: types.startShake }) - .doOnNext(dispatch); + if (positionX <= -threshold) { + finalAnswer = false; } + } else { + finalAnswer = userAnswer; + } - if (tests[currentQuestion]) { - return Observable - .just({ type: types.goToNextQuestion }) - .delay(300) - .startWith({ type: types.primeNextQuestion }); - } - - let updateUser$; - if (isSignedIn) { - const body = { id, name, challengeType }; - updateUser$ = postJSON$('/completed-challenge', body) - // if post fails, will retry once - .retry(3) - .flatMap(({ alreadyCompleted, points }) => { - return Observable.of( - makeToast({ - message: - 'Challenge saved.' + - (alreadyCompleted ? '' : ' First time Completed!'), - title: 'Saved', - type: 'info' - }), - updatePoints(points), - ); - }) - .catch(error => { - return Observable.just({ - type: 'app.error', - error - }); - }); - } else { - updateUser$ = Observable.empty(); - } - - const challengeCompleted$ = Observable.of( - goToNextHike(), - makeToast({ - title: 'Congratulations!', - message: 'Hike completed.' + (isSignedIn ? ' Saving...' : ''), - type: 'success' - }) - ); - - return Observable.merge(challengeCompleted$, updateUser$) - .delay(300) - .startWith(hikeCompleted(finalAnswer)) - .catch(error => Observable.just({ - type: 'error', - error + // incorrect question + if (answer !== finalAnswer) { + if (info) { + dispatch(makeToast({ + title: 'Hint', + message: info, + type: 'info' })); + } + + return Observable + .just({ type: types.endShake }) + .delay(500) + .startWith({ type: types.startShake }) + .doOnNext(dispatch); + } + + if (tests[currentQuestion]) { + return Observable + .just({ type: types.goToNextQuestion }) + .delay(300) + .startWith({ type: types.primeNextQuestion }) + .doOnNext(dispatch); + } + + let updateUser$; + if (isSignedIn) { + const body = { id, name, challengeType }; + updateUser$ = postJSON$('/completed-challenge', body) + // if post fails, will retry once + .retry(3) + .flatMap(({ alreadyCompleted, points }) => { + return Observable.of( + makeToast({ + message: + 'Challenge saved.' + + (alreadyCompleted ? '' : ' First time Completed!'), + title: 'Saved', + type: 'info' + }), + updatePoints(points), + ); + }) + .catch(error => { + return Observable.just({ + type: 'app.error', + error + }); + }); + } else { + updateUser$ = Observable.empty(); + } + + const challengeCompleted$ = Observable.of( + goToNextHike(), + makeToast({ + title: 'Congratulations!', + message: 'Hike completed.' + (isSignedIn ? ' Saving...' : ''), + type: 'success' + }) + ); + + return Observable.merge(challengeCompleted$, updateUser$) + .delay(300) + .startWith(hikeCompleted(finalAnswer)) + .catch(error => Observable.just({ + type: 'error', + error + })) + .doOnNext(dispatch); +} + +export default () => ({ getState, dispatch }) => next => { + return function answerSaga(action) { + if (action.type === types.answerQuestion) { + return handleAnswer(getState, dispatch, next, action); + } + + // let goToNextQuestion hit reducers first + const result = next(action); + if (action.type === types.goToNextHike) { + const { hikesApp: { currentHike } } = getState(); + // if no next hike currentHike will equal '' which is falsy + if (currentHike) { + dispatch(push(`/videos/${currentHike}`)); + } else { + dispatch(push('/map')); + } + } + + return result; }; }; diff --git a/common/app/routes/Hikes/redux/reducer.js b/common/app/routes/Hikes/redux/reducer.js index 3cc84b0c38a..44565c0de12 100644 --- a/common/app/routes/Hikes/redux/reducer.js +++ b/common/app/routes/Hikes/redux/reducer.js @@ -1,6 +1,6 @@ import { handleActions } from 'redux-actions'; import types from './types'; -import { findNextHike } from './utils'; +import { findNextHikeName } from './utils'; const initialState = { hikes: { @@ -79,7 +79,7 @@ export default handleActions( [types.goToNextHike]: state => ({ ...state, - currentHike: findNextHike(state.hikes, state.currentHike), + currentHike: findNextHikeName(state.hikes, state.currentHike), showQuestions: false, currentQuestion: 1, mouse: [ 0, 0 ] diff --git a/common/app/routes/Hikes/redux/utils.js b/common/app/routes/Hikes/redux/utils.js index d03b33dc9aa..e9fc5bd777e 100644 --- a/common/app/routes/Hikes/redux/utils.js +++ b/common/app/routes/Hikes/redux/utils.js @@ -42,21 +42,24 @@ export function getCurrentHike(hikes = {}, dashedName) { return hikes.entities[dashedName]; } -export function findNextHike({ entities, results }, dashedName) { +// findNextHikeName( +// hikes: { results: String[] }, +// dashedName: String +// ) => String +export function findNextHikeName({ results }, dashedName) { if (!dashedName) { log('find next hike no id provided'); - return entities[results[0]]; + return results[0]; } const currentIndex = _.findIndex( results, - ({ dashedName: _dashedName }) => _dashedName === dashedName + _dashedName => _dashedName === dashedName ); if (currentIndex >= results.length) { return ''; } - - return entities[results[currentIndex + 1]]; + return results[currentIndex + 1]; }