Hikes loading next hike

pull/7214/head
Berkeley Martinez 2016-02-05 16:40:02 -08:00
parent c4ba7ac46a
commit 1e9c9baedd
4 changed files with 156 additions and 117 deletions

View File

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

View File

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

View File

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

View File

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