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) {
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;

View File

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

View File

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

View File

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

View File

@ -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({

View File

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