Question now semi functional
This commit is contained in:
@ -26,7 +26,7 @@ const appLocation = createLocation(
|
||||
function location$(history) {
|
||||
return Rx.Observable.create(function(observer) {
|
||||
const dispose = history.listen(function(location) {
|
||||
observer.onNext(location.pathname);
|
||||
observer.onNext(location);
|
||||
});
|
||||
|
||||
return Rx.Disposable.create(() => {
|
||||
@ -40,10 +40,9 @@ 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
|
||||
@ -51,12 +50,26 @@ app$({ history, location: appLocation })
|
||||
)
|
||||
.doOnNext(({ appCat }) => {
|
||||
const appActions = appCat.getActions('appActions');
|
||||
const appStore = appCat.getStore('appStore');
|
||||
|
||||
location$(history)
|
||||
const route$ = location$(history)
|
||||
.pluck('pathname')
|
||||
.distinctUntilChanged()
|
||||
.doOnNext(route => debug('route change', route))
|
||||
.subscribe(route => appActions.updateRoute(route));
|
||||
.distinctUntilChanged();
|
||||
|
||||
appStore
|
||||
.pluck('route')
|
||||
.filter(route => !!route)
|
||||
.withLatestFrom(
|
||||
route$,
|
||||
(nextRoute, currentRoute) => ({ currentRoute, nextRoute })
|
||||
)
|
||||
// only continue when route change requested
|
||||
.filter(({ currentRoute, nextRoute }) => currentRoute !== nextRoute)
|
||||
.doOnNext(({ nextRoute }) => {
|
||||
debug('route change', nextRoute);
|
||||
history.pushState(history.state, nextRoute);
|
||||
})
|
||||
.subscribeOnError(err => console.error(err));
|
||||
|
||||
appActions.goBack.subscribe(function() {
|
||||
history.goBack();
|
||||
@ -65,10 +78,11 @@ app$({ history, location: appLocation })
|
||||
appActions
|
||||
.updateRoute
|
||||
.pluck('route')
|
||||
.doOnNext(route => debug('update route', route))
|
||||
.subscribe(function(route) {
|
||||
history.pushState(null, route);
|
||||
});
|
||||
.doOnNext(route => {
|
||||
debug('update route', route);
|
||||
history.pushState(history.state, route);
|
||||
})
|
||||
.subscribeOnError(err => console.error(err));
|
||||
})
|
||||
.flatMap(({ props, appCat }) => {
|
||||
props.history = history;
|
||||
|
@ -2,14 +2,15 @@ import { Cat } from 'thundercats';
|
||||
import stamp from 'stampit';
|
||||
import { Disposable, Observable } from 'rx';
|
||||
|
||||
import { postJSON$ } from '../utils/ajax-stream.js';
|
||||
import { post$, 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$
|
||||
postJSON$,
|
||||
post$
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -8,8 +8,8 @@ const initValue = {
|
||||
points: 0,
|
||||
hikesApp: {
|
||||
hikes: [],
|
||||
currentHikes: {},
|
||||
currentQuestion: 1,
|
||||
// lecture state
|
||||
currentHike: {},
|
||||
showQuestion: false
|
||||
}
|
||||
};
|
||||
@ -22,13 +22,31 @@ export default Store({
|
||||
init({ instance: appStore, args: [cat] }) {
|
||||
const { updateRoute, getUser, setTitle } = cat.getActions('appActions');
|
||||
const register = createRegistrar(appStore);
|
||||
const { toggleQuestions, fetchHikes } = cat.getActions('hikesActions');
|
||||
const {
|
||||
toggleQuestions,
|
||||
fetchHikes,
|
||||
hideInfo,
|
||||
grabQuestion,
|
||||
releaseQuestion,
|
||||
moveQuestion,
|
||||
answer
|
||||
} = cat.getActions('hikesActions');
|
||||
|
||||
// app
|
||||
register(setter(fromMany(getUser, setTitle, updateRoute)));
|
||||
|
||||
// hikes
|
||||
register(fromMany(fetchHikes, toggleQuestions));
|
||||
register(
|
||||
fromMany(
|
||||
toggleQuestions,
|
||||
fetchHikes,
|
||||
hideInfo,
|
||||
grabQuestion,
|
||||
releaseQuestion,
|
||||
moveQuestion,
|
||||
answer
|
||||
)
|
||||
);
|
||||
|
||||
return appStore;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Motion } from 'react-motion';
|
||||
import { spring, Motion } from 'react-motion';
|
||||
import { contain } from 'thundercats-react';
|
||||
import debugFactory from 'debug';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
@ -10,19 +9,32 @@ import {
|
||||
Row
|
||||
} from 'react-bootstrap';
|
||||
|
||||
const debug = debugFactory('freecc:hikes');
|
||||
const ANSWER_THRESHOLD = 200;
|
||||
|
||||
export default contain(
|
||||
{
|
||||
store: 'appStore',
|
||||
actions: ['hikesAction'],
|
||||
map(state) {
|
||||
const { currentQuestion, currentHike } = state.hikesApp;
|
||||
|
||||
actions: ['hikesActions'],
|
||||
map({ hikesApp }) {
|
||||
const {
|
||||
currentHike,
|
||||
currentQuestion = 1,
|
||||
mouse = [0, 0],
|
||||
isCorrect = false,
|
||||
delta = [0, 0],
|
||||
isPressed = false,
|
||||
showInfo = false,
|
||||
shake = false
|
||||
} = hikesApp;
|
||||
return {
|
||||
hike: currentHike,
|
||||
currentQuestion
|
||||
currentQuestion,
|
||||
mouse,
|
||||
isCorrect,
|
||||
delta,
|
||||
isPressed,
|
||||
showInfo,
|
||||
shake
|
||||
};
|
||||
}
|
||||
},
|
||||
@ -30,150 +42,89 @@ export default contain(
|
||||
displayName: 'Questions',
|
||||
|
||||
propTypes: {
|
||||
dashedName: PropTypes.string,
|
||||
currentQuestion: PropTypes.number,
|
||||
hike: PropTypes.object,
|
||||
currentQuestion: PropTypes.number,
|
||||
mouse: PropTypes.array,
|
||||
isCorrect: PropTypes.bool,
|
||||
delta: PropTypes.array,
|
||||
isPressed: PropTypes.bool,
|
||||
showInfo: PropTypes.bool,
|
||||
shake: PropTypes.bool,
|
||||
hikesActions: 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]
|
||||
});
|
||||
const { mouse: [pressX, pressY], hikesActions } = this.props;
|
||||
hikesActions.grabQuestion({ pressX, pressY, pageX, pageY });
|
||||
},
|
||||
|
||||
handleMouseUp() {
|
||||
const { correct } = this.state;
|
||||
if (correct) {
|
||||
return this.setState({
|
||||
isPressed: false,
|
||||
delta: [0, 0]
|
||||
});
|
||||
if (!this.props.isPressed) {
|
||||
return null;
|
||||
}
|
||||
this.setState({
|
||||
isPressed: false,
|
||||
mouse: [0, 0],
|
||||
delta: [0, 0]
|
||||
});
|
||||
this.props.hikesActions.releaseQuestion();
|
||||
},
|
||||
|
||||
handleMouseMove(answer) {
|
||||
if (!this.props.isPressed) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
return (e) => {
|
||||
let { pageX, pageY, touches } = e;
|
||||
|
||||
if (touches) {
|
||||
e.preventDefault();
|
||||
// these reassins the values of pageX, pageY from touches
|
||||
// these re-assigns 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 });
|
||||
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);
|
||||
};
|
||||
},
|
||||
|
||||
hideInfo() {
|
||||
this.setState({ showInfo: false });
|
||||
},
|
||||
|
||||
onAnswer(answer, userAnswer) {
|
||||
const { hikesActions } = this.props;
|
||||
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
|
||||
);
|
||||
return hikesActions.answer({ answer, userAnswer, props: this.props });
|
||||
};
|
||||
},
|
||||
|
||||
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,
|
||||
isCorrect: false,
|
||||
mouse: [0, 0]
|
||||
}, cb);
|
||||
},
|
||||
|
||||
renderInfo(showInfo, info) {
|
||||
renderInfo(showInfo, info, hideInfo) {
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
backdrop={ true }
|
||||
onHide={ this.hideInfo }
|
||||
onHide={ hideInfo }
|
||||
show={ showInfo }>
|
||||
<Modal.Body>
|
||||
<h3>
|
||||
@ -184,7 +135,7 @@ export default contain(
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='large'
|
||||
onClick={ this.hideInfo }>
|
||||
onClick={ hideInfo }>
|
||||
hide
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
@ -193,8 +144,7 @@ export default contain(
|
||||
},
|
||||
|
||||
renderQuestion(number, question, answer, shake) {
|
||||
return ({ x: xFunc }) => {
|
||||
const x = xFunc().val.x;
|
||||
return ({ x }) => {
|
||||
const style = {
|
||||
WebkitTransform: `translate3d(${ x }px, 0, 0)`,
|
||||
transform: `translate3d(${ x }px, 0, 0)`
|
||||
@ -219,10 +169,12 @@ export default contain(
|
||||
},
|
||||
|
||||
render() {
|
||||
const { showInfo, shake } = this.state;
|
||||
const { showInfo, shake } = this.props;
|
||||
const {
|
||||
hike: { tests = [] } = {},
|
||||
currentQuestion
|
||||
mouse: [x],
|
||||
currentQuestion,
|
||||
hikesActions
|
||||
} = this.props;
|
||||
|
||||
const [ question, answer, info ] = tests[currentQuestion - 1] || [];
|
||||
@ -233,21 +185,21 @@ export default contain(
|
||||
xs={ 8 }
|
||||
xsOffset={ 2 }>
|
||||
<Row>
|
||||
<Motion style={{ x: this.getTweenValues }}>
|
||||
<Motion style={{ x: spring(x, [120, 10]) }}>
|
||||
{ this.renderQuestion(currentQuestion, question, answer, shake) }
|
||||
</Motion>
|
||||
{ this.renderInfo(showInfo, info) }
|
||||
{ this.renderInfo(showInfo, info, hikesActions.hideInfo) }
|
||||
<Panel>
|
||||
<Button
|
||||
bsSize='large'
|
||||
className='pull-left'
|
||||
onClick={ this.onAnswer(answer, false, info) }>
|
||||
onClick={ this.onAnswer(answer, false) }>
|
||||
false
|
||||
</Button>
|
||||
<Button
|
||||
bsSize='large'
|
||||
className='pull-right'
|
||||
onClick={ this.onAnswer(answer, true, info) }>
|
||||
onClick={ this.onAnswer(answer, true) }>
|
||||
true
|
||||
</Button>
|
||||
</Panel>
|
||||
|
@ -35,6 +35,20 @@ function findNextHike(hikes, 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({
|
||||
refs: { displayName: 'HikesActions' },
|
||||
shouldBindMethods: true,
|
||||
@ -74,14 +88,111 @@ export default Actions({
|
||||
toggleQuestions() {
|
||||
return {
|
||||
transform(state) {
|
||||
state.hikesApp.showQuestions = !state.hikesApp.showQuestions;
|
||||
return Object.assign({}, state);
|
||||
const hikesApp = { ...state.hikesApp, showQuestions: true };
|
||||
return { ...state, hikesApp };
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
completedHike(data = {}) {
|
||||
return this.postJSON$('/completed-challenge', data)
|
||||
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
|
||||
}
|
||||
}) {
|
||||
|
||||
// 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
|
||||
if (tests[currentQuestion + 1]) {
|
||||
|
||||
return {
|
||||
transform(state) {
|
||||
|
||||
const hikesApp = {
|
||||
...state.hikesApp,
|
||||
currentQuestion: currentQuestion + 1
|
||||
};
|
||||
|
||||
return { ...state, hikesApp };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// challenge completed
|
||||
const correctAnswer = {
|
||||
transform(state) {
|
||||
const hikesApp = {
|
||||
...state.hikesApp,
|
||||
isCorrect: true,
|
||||
isPressed: false,
|
||||
delta: [0, 0],
|
||||
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||
};
|
||||
return { ...state, hikesApp };
|
||||
}
|
||||
};
|
||||
|
||||
return this.post$('/completed-challenge', { id, name, challengeType })
|
||||
.map(() => {
|
||||
return {
|
||||
transform(state) {
|
||||
@ -98,6 +209,7 @@ export default Actions({
|
||||
}
|
||||
};
|
||||
})
|
||||
.startWith(correctAnswer)
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
return Observable.just({
|
||||
|
@ -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' }
|
||||
|
Reference in New Issue
Block a user