Question now semi functional

This commit is contained in:
Berkeley Martinez
2015-12-29 17:35:50 -08:00
parent 3fd472e594
commit 54bb926c3d
6 changed files with 252 additions and 139 deletions

View File

@ -26,7 +26,7 @@ const appLocation = createLocation(
function location$(history) { function location$(history) {
return Rx.Observable.create(function(observer) { return Rx.Observable.create(function(observer) {
const dispose = history.listen(function(location) { const dispose = history.listen(function(location) {
observer.onNext(location.pathname); observer.onNext(location);
}); });
return Rx.Disposable.create(() => { return Rx.Disposable.create(() => {
@ -40,10 +40,9 @@ app$({ history, location: appLocation })
.flatMap( .flatMap(
({ AppCat }) => { ({ AppCat }) => {
// instantiate the cat with service // instantiate the cat with service
const appCat = AppCat(null, services); const appCat = AppCat(null, services, history);
// hydrate the stores // hydrate the stores
return hydrate(appCat, catState) return hydrate(appCat, catState).map(() => appCat);
.map(() => appCat);
}, },
// not using nextLocation at the moment but will be used for // not using nextLocation at the moment but will be used for
// redirects in the future // redirects in the future
@ -51,12 +50,26 @@ app$({ history, location: appLocation })
) )
.doOnNext(({ appCat }) => { .doOnNext(({ appCat }) => {
const appActions = appCat.getActions('appActions'); const appActions = appCat.getActions('appActions');
const appStore = appCat.getStore('appStore');
location$(history) const route$ = location$(history)
.pluck('pathname') .pluck('pathname')
.distinctUntilChanged() .distinctUntilChanged();
.doOnNext(route => debug('route change', route))
.subscribe(route => appActions.updateRoute(route)); 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() { appActions.goBack.subscribe(function() {
history.goBack(); history.goBack();
@ -65,10 +78,11 @@ app$({ history, location: appLocation })
appActions appActions
.updateRoute .updateRoute
.pluck('route') .pluck('route')
.doOnNext(route => debug('update route', route)) .doOnNext(route => {
.subscribe(function(route) { debug('update route', route);
history.pushState(null, route); history.pushState(history.state, route);
}); })
.subscribeOnError(err => console.error(err));
}) })
.flatMap(({ props, appCat }) => { .flatMap(({ props, appCat }) => {
props.history = history; props.history = history;

View File

@ -2,14 +2,15 @@ import { Cat } from 'thundercats';
import stamp from 'stampit'; import stamp from 'stampit';
import { Disposable, Observable } from 'rx'; 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 { AppActions, AppStore } from './flux';
import { HikesActions } from './routes/Hikes/flux'; import { HikesActions } from './routes/Hikes/flux';
import { JobActions, JobsStore} from './routes/Jobs/flux'; import { JobActions, JobsStore} from './routes/Jobs/flux';
const ajaxStamp = stamp({ const ajaxStamp = stamp({
methods: { methods: {
postJSON$: postJSON$ postJSON$,
post$
} }
}); });

View File

@ -8,8 +8,8 @@ const initValue = {
points: 0, points: 0,
hikesApp: { hikesApp: {
hikes: [], hikes: [],
currentHikes: {}, // lecture state
currentQuestion: 1, currentHike: {},
showQuestion: false showQuestion: false
} }
}; };
@ -22,13 +22,31 @@ export default Store({
init({ instance: appStore, args: [cat] }) { init({ instance: appStore, args: [cat] }) {
const { updateRoute, getUser, setTitle } = cat.getActions('appActions'); const { updateRoute, getUser, setTitle } = cat.getActions('appActions');
const register = createRegistrar(appStore); const register = createRegistrar(appStore);
const { toggleQuestions, fetchHikes } = cat.getActions('hikesActions'); const {
toggleQuestions,
fetchHikes,
hideInfo,
grabQuestion,
releaseQuestion,
moveQuestion,
answer
} = cat.getActions('hikesActions');
// app // app
register(setter(fromMany(getUser, setTitle, updateRoute))); register(setter(fromMany(getUser, setTitle, updateRoute)));
// hikes // hikes
register(fromMany(fetchHikes, toggleQuestions)); register(
fromMany(
toggleQuestions,
fetchHikes,
hideInfo,
grabQuestion,
releaseQuestion,
moveQuestion,
answer
)
);
return appStore; return appStore;
} }

View File

@ -1,7 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Motion } from 'react-motion'; import { spring, Motion } from 'react-motion';
import { contain } from 'thundercats-react'; import { contain } from 'thundercats-react';
import debugFactory from 'debug';
import { import {
Button, Button,
Col, Col,
@ -10,19 +9,32 @@ import {
Row Row
} from 'react-bootstrap'; } from 'react-bootstrap';
const debug = debugFactory('freecc:hikes');
const ANSWER_THRESHOLD = 200; const ANSWER_THRESHOLD = 200;
export default contain( export default contain(
{ {
store: 'appStore', store: 'appStore',
actions: ['hikesAction'], actions: ['hikesActions'],
map(state) { map({ hikesApp }) {
const { currentQuestion, currentHike } = state.hikesApp; const {
currentHike,
currentQuestion = 1,
mouse = [0, 0],
isCorrect = false,
delta = [0, 0],
isPressed = false,
showInfo = false,
shake = false
} = hikesApp;
return { return {
hike: currentHike, hike: currentHike,
currentQuestion currentQuestion,
mouse,
isCorrect,
delta,
isPressed,
showInfo,
shake
}; };
} }
}, },
@ -30,150 +42,89 @@ export default contain(
displayName: 'Questions', displayName: 'Questions',
propTypes: { propTypes: {
dashedName: PropTypes.string,
currentQuestion: PropTypes.number,
hike: PropTypes.object, 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 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 }) { handleMouseDown({ pageX, pageY, touches }) {
if (touches) { if (touches) {
({ pageX, pageY } = touches[0]); ({ pageX, pageY } = touches[0]);
} }
const { mouse: [pressX, pressY] } = this.state; const { mouse: [pressX, pressY], hikesActions } = this.props;
const dx = pageX - pressX; hikesActions.grabQuestion({ pressX, pressY, pageX, pageY });
const dy = pageY - pressY;
this.setState({
isPressed: true,
delta: [dx, dy],
mouse: [pageX - dx, pageY - dy]
});
}, },
handleMouseUp() { handleMouseUp() {
const { correct } = this.state; if (!this.props.isPressed) {
if (correct) { return null;
return this.setState({
isPressed: false,
delta: [0, 0]
});
} }
this.setState({ this.props.hikesActions.releaseQuestion();
isPressed: false,
mouse: [0, 0],
delta: [0, 0]
});
}, },
handleMouseMove(answer) { handleMouseMove(answer) {
if (!this.props.isPressed) {
return () => {};
}
return (e) => { return (e) => {
let { pageX, pageY, touches } = e; let { pageX, pageY, touches } = e;
if (touches) { if (touches) {
e.preventDefault(); 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]); ({ pageX, pageY } = touches[0]);
} }
const { isPressed, delta: [dx, dy] } = this.state; const { delta: [dx, dy], hikesActions } = this.props;
if (isPressed) { const mouse = [pageX - dx, pageY - dy];
const mouse = [pageX - dx, pageY - dy];
if (mouse[0] >= ANSWER_THRESHOLD) { if (mouse[0] >= ANSWER_THRESHOLD) {
this.handleMouseUp(); return this.onAnswer(answer, true)();
return this.onAnswer(answer, true)();
}
if (mouse[0] <= -ANSWER_THRESHOLD) {
this.handleMouseUp();
return this.onAnswer(answer, false)();
}
this.setState({ mouse });
} }
if (mouse[0] <= -ANSWER_THRESHOLD) {
return this.onAnswer(answer, false)();
}
return hikesActions.moveQuestion(mouse);
}; };
}, },
hideInfo() {
this.setState({ showInfo: false });
},
onAnswer(answer, userAnswer) { onAnswer(answer, userAnswer) {
const { hikesActions } = this.props;
return (e) => { return (e) => {
if (e && e.preventDefault) { if (e && e.preventDefault) {
e.preventDefault(); e.preventDefault();
} }
if (this.disposeTimeout) { return hikesActions.answer({ answer, userAnswer, props: this.props });
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) { routerWillLeave(nextState, router, cb) {
// TODO(berks): do animated transitions here stuff here // TODO(berks): do animated transitions here stuff here
this.setState({ this.setState({
showInfo: false, showInfo: false,
correct: false, isCorrect: false,
mouse: [0, 0] mouse: [0, 0]
}, cb); }, cb);
}, },
renderInfo(showInfo, info) { renderInfo(showInfo, info, hideInfo) {
if (!info) { if (!info) {
return null; return null;
} }
return ( return (
<Modal <Modal
backdrop={ true } backdrop={ true }
onHide={ this.hideInfo } onHide={ hideInfo }
show={ showInfo }> show={ showInfo }>
<Modal.Body> <Modal.Body>
<h3> <h3>
@ -184,7 +135,7 @@ export default contain(
<Button <Button
block={ true } block={ true }
bsSize='large' bsSize='large'
onClick={ this.hideInfo }> onClick={ hideInfo }>
hide hide
</Button> </Button>
</Modal.Footer> </Modal.Footer>
@ -193,8 +144,7 @@ export default contain(
}, },
renderQuestion(number, question, answer, shake) { renderQuestion(number, question, answer, shake) {
return ({ x: xFunc }) => { return ({ x }) => {
const x = xFunc().val.x;
const style = { const style = {
WebkitTransform: `translate3d(${ x }px, 0, 0)`, WebkitTransform: `translate3d(${ x }px, 0, 0)`,
transform: `translate3d(${ x }px, 0, 0)` transform: `translate3d(${ x }px, 0, 0)`
@ -219,10 +169,12 @@ export default contain(
}, },
render() { render() {
const { showInfo, shake } = this.state; const { showInfo, shake } = this.props;
const { const {
hike: { tests = [] } = {}, hike: { tests = [] } = {},
currentQuestion mouse: [x],
currentQuestion,
hikesActions
} = this.props; } = this.props;
const [ question, answer, info ] = tests[currentQuestion - 1] || []; const [ question, answer, info ] = tests[currentQuestion - 1] || [];
@ -233,21 +185,21 @@ export default contain(
xs={ 8 } xs={ 8 }
xsOffset={ 2 }> xsOffset={ 2 }>
<Row> <Row>
<Motion style={{ x: this.getTweenValues }}> <Motion style={{ x: spring(x, [120, 10]) }}>
{ this.renderQuestion(currentQuestion, question, answer, shake) } { this.renderQuestion(currentQuestion, question, answer, shake) }
</Motion> </Motion>
{ this.renderInfo(showInfo, info) } { this.renderInfo(showInfo, info, hikesActions.hideInfo) }
<Panel> <Panel>
<Button <Button
bsSize='large' bsSize='large'
className='pull-left' className='pull-left'
onClick={ this.onAnswer(answer, false, info) }> onClick={ this.onAnswer(answer, false) }>
false false
</Button> </Button>
<Button <Button
bsSize='large' bsSize='large'
className='pull-right' className='pull-right'
onClick={ this.onAnswer(answer, true, info) }> onClick={ this.onAnswer(answer, true) }>
true true
</Button> </Button>
</Panel> </Panel>

View File

@ -35,6 +35,20 @@ function findNextHike(hikes, id) {
return hikes[currentIndex + 1] || hikes[0]; 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({ export default Actions({
refs: { displayName: 'HikesActions' }, refs: { displayName: 'HikesActions' },
shouldBindMethods: true, shouldBindMethods: true,
@ -74,14 +88,111 @@ export default Actions({
toggleQuestions() { toggleQuestions() {
return { return {
transform(state) { transform(state) {
state.hikesApp.showQuestions = !state.hikesApp.showQuestions; const hikesApp = { ...state.hikesApp, showQuestions: true };
return Object.assign({}, state); return { ...state, hikesApp };
} }
}; };
}, },
completedHike(data = {}) { hideInfo() {
return this.postJSON$('/completed-challenge', data) 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(() => { .map(() => {
return { return {
transform(state) { transform(state) {
@ -98,6 +209,7 @@ export default Actions({
} }
}; };
}) })
.startWith(correctAnswer)
.catch(err => { .catch(err => {
console.error(err); console.error(err);
return Observable.just({ return Observable.just({

View File

@ -17,7 +17,7 @@
*/ */
import debugFactory from 'debug'; import debugFactory from 'debug';
import { AnonymousObservable, helpers } from 'rx'; import { Observable, AnonymousObservable, helpers } from 'rx';
const debug = debugFactory('freecc:ajax$'); const debug = debugFactory('freecc:ajax$');
const root = typeof window !== 'undefined' ? window : {}; const root = typeof window !== 'undefined' ? window : {};
@ -147,8 +147,12 @@ export function ajax$(options) {
var processResponse = function(xhr, e) { var processResponse = function(xhr, e) {
var status = xhr.status === 1223 ? 204 : xhr.status; var status = xhr.status === 1223 ? 204 : xhr.status;
if ((status >= 200 && status <= 300) || status === 0 || status === '') { if ((status >= 200 && status <= 300) || status === 0 || status === '') {
observer.onNext(normalizeSuccess(e, xhr, settings)); try {
observer.onCompleted(); observer.onNext(normalizeSuccess(e, xhr, settings));
observer.onCompleted();
} catch (err) {
observer.onError(err);
}
} else { } else {
observer.onError(normalizeError(e, xhr, 'error')); observer.onError(normalizeError(e, xhr, 'error'));
} }
@ -228,8 +232,8 @@ export function ajax$(options) {
settings.hasContent && settings.body settings.hasContent && settings.body
); );
xhr.send(settings.hasContent && settings.body || null); xhr.send(settings.hasContent && settings.body || null);
} catch (e) { } catch (err) {
observer.onError(e); observer.onError(err);
} }
return function() { return function() {
@ -247,13 +251,25 @@ export function ajax$(options) {
* from the Ajax POST. * from the Ajax POST.
*/ */
export function post$(url, body) { export function post$(url, body) {
try {
body = JSON.stringify(body);
} catch (e) {
return Observable.throw(e);
}
return ajax$({ url, body, method: 'POST' }); return ajax$({ url, body, method: 'POST' });
} }
export function postJSON$(url, body) { export function postJSON$(url, body) {
try {
body = JSON.stringify(body);
} catch (e) {
return Observable.throw(e);
}
return ajax$({ return ajax$({
url, url,
body: JSON.stringify(body), body,
method: 'POST', method: 'POST',
responseType: 'json', responseType: 'json',
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }