diff --git a/common/app/App.jsx b/common/app/App.jsx
index 0afb196a5b..2041bcf47d 100644
--- a/common/app/App.jsx
+++ b/common/app/App.jsx
@@ -63,6 +63,7 @@ export class FreeCodeCamp extends React.Component {
render() {
const { username, points, picture } = this.props;
const navProps = { username, points, picture };
+ console.log('app', this.props.children);
return (
diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/Hikes/components/Hike.jsx
index 6f75ae7f9b..55c3ac623f 100644
--- a/common/app/routes/Hikes/components/Hike.jsx
+++ b/common/app/routes/Hikes/components/Hike.jsx
@@ -17,6 +17,7 @@ const mapStateToProps = createSelector(
};
}
);
+
// export plain component for testing
export class Hike extends React.Component {
static displayName = 'Hike';
@@ -71,4 +72,4 @@ export class Hike extends React.Component {
}
// export redux aware component
-export default connect(mapStateToProps, { resetHike });
+export default connect(mapStateToProps, { resetHike })(Hike);
diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx
index 17c027a4de..7c3aa04d4a 100644
--- a/common/app/routes/Hikes/components/Hikes.jsx
+++ b/common/app/routes/Hikes/components/Hikes.jsx
@@ -21,10 +21,11 @@ const mapStateToProps = createSelector(
return { hikes: [] };
}
return {
- hikes: hikes.results.map(dashedName => hikes.enitites[dashedName])
+ hikes: hikes.results.map(dashedName => hikes.entities[dashedName])
};
}
);
+
const fetchOptions = {
fetchAction: 'fetchHikes',
@@ -50,8 +51,6 @@ export class Hikes extends React.Component {
updateTitle('Hikes');
}
- shouldComponentUpdate = shouldComponentUpdate;
-
renderMap(hikes) {
return (
diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx
index 1aac07f492..688cdc4b08 100644
--- a/common/app/routes/Hikes/components/Lecture.jsx
+++ b/common/app/routes/Hikes/components/Lecture.jsx
@@ -90,4 +90,4 @@ export class Lecture extends React.Component {
}
}
-export default connect(mapStateToProps, { })(Lecture);
+export default connect(mapStateToProps)(Lecture);
diff --git a/common/app/routes/Hikes/components/Map.jsx b/common/app/routes/Hikes/components/Map.jsx
index 5d4fc98dfe..81d77432c0 100644
--- a/common/app/routes/Hikes/components/Map.jsx
+++ b/common/app/routes/Hikes/components/Map.jsx
@@ -17,7 +17,7 @@ export default React.createClass({
const vidElements = hikes.map(({ title, dashedName}) => {
return (
-
+
{ title }
diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx
index 1982391d92..7404abd5ed 100644
--- a/common/app/routes/Hikes/components/Questions.jsx
+++ b/common/app/routes/Hikes/components/Questions.jsx
@@ -1,177 +1,188 @@
import React, { PropTypes } from 'react';
import { spring, Motion } from 'react-motion';
-import { contain } from 'thundercats-react';
+import { connect } from 'react-redux';
import { Button, Col, Row } from 'react-bootstrap';
+import { createSelector } from 'reselect';
+
+import {
+ answerQuestion,
+ moveQuestion,
+ releaseQuestion,
+ grabQuestion
+} from '../redux/actions';
const answerThreshold = 100;
+const actionsToBind = {
+ answerQuestion,
+ moveQuestion,
+ releaseQuestion,
+ grabQuestion
+};
-export default contain(
- {
- store: 'appStore',
- actions: ['hikesActions'],
- map({ hikesApp, username }) {
- const {
- currentHike,
- currentQuestion = 1,
- mouse = [0, 0],
- isCorrect = false,
- delta = [0, 0],
- isPressed = false,
- shake = false
- } = hikesApp;
- return {
- hike: currentHike,
- currentQuestion,
- mouse,
- isCorrect,
- delta,
- isPressed,
- shake,
- isSignedIn: !!username
- };
- }
- },
- React.createClass({
- displayName: 'Questions',
+const mapStateToProps = createSelector(
+ state => state.hikesApp.hikes.entities,
+ state => state.hikesApp.hikes.results,
+ state => state.hikesApp.ui,
+ state => state.app.isSignedIn,
+ (hikesMap, hikesByDashname, ui, isSignedIn) => {
+ const {
+ currentQuestion = 1,
+ mouse = [ 0, 0 ],
+ delta = [ 0, 0 ],
+ isCorrect = false,
+ isPressed = false,
+ shouldShakeQuestion = false
+ } = ui;
- propTypes: {
- hike: PropTypes.object,
- currentQuestion: PropTypes.number,
- mouse: PropTypes.array,
- isCorrect: PropTypes.bool,
- delta: PropTypes.array,
- isPressed: PropTypes.bool,
- shake: PropTypes.bool,
- isSignedIn: PropTypes.bool,
- hikesActions: PropTypes.object
- },
-
- handleMouseUp(e, answer, info) {
- e.stopPropagation();
- if (!this.props.isPressed) {
- return null;
- }
-
- const {
- hike,
- currentQuestion,
- isSignedIn,
- delta
- } = this.props;
-
- this.props.hikesActions.releaseQuestion();
- this.props.hikesActions.answer({
- e,
- answer,
- hike,
- delta,
- currentQuestion,
- isSignedIn,
- info,
- threshold: answerThreshold
- });
- },
-
- handleMouseMove(e) {
- if (!this.props.isPressed) {
- return null;
- }
- const { delta, hikesActions } = this.props;
-
- hikesActions.moveQuestion({ e, delta });
- },
-
- onAnswer(answer, userAnswer, info) {
- const { isSignedIn, hike, currentQuestion, hikesActions } = this.props;
- return (e) => {
- if (e && e.preventDefault) {
- e.preventDefault();
- }
-
- return hikesActions.answer({
- answer,
- userAnswer,
- currentQuestion,
- hike,
- info,
- isSignedIn
- });
- };
- },
-
- renderQuestion(number, question, answer, shake, info) {
- const { hikesActions } = this.props;
- const mouseUp = e => this.handleMouseUp(e, answer, info);
- return ({ x }) => {
- const style = {
- WebkitTransform: `translate3d(${ x }px, 0, 0)`,
- transform: `translate3d(${ x }px, 0, 0)`
- };
- return (
-
- Question { number }
- { question }
-
- );
- };
- },
-
- render() {
- const {
- hike: { tests = [] } = {},
- mouse: [x],
- currentQuestion,
- shake
- } = this.props;
-
- const [ question, answer, info ] = tests[currentQuestion - 1] || [];
- const questionElement = this.renderQuestion(
- currentQuestion,
- question,
- answer,
- shake,
- info
- );
-
- return (
-
this.handleMouseUp(e, answer, info) }
- xs={ 8 }
- xsOffset={ 2 }>
-
-
- { questionElement }
-
-
-
-
-
-
-
-
-
- );
- }
- })
+ return {
+ currentQuestion,
+ isCorrect,
+ mouse,
+ delta,
+ isPressed,
+ shouldShakeQuestion,
+ isSignedIn
+ };
+ }
);
+
+class Question extends React.Component {
+ static displayName = 'Questions';
+
+ static propTypes = {
+ // actions
+ answerQuestion: PropTypes.func,
+ releaseQuestion: PropTypes.func,
+ moveQuestion: PropTypes.func,
+ grabQuestion: PropTypes.func,
+ // ui state
+ tests: PropTypes.array,
+ mouse: PropTypes.array,
+ delta: PropTypes.array,
+ isCorrect: PropTypes.bool,
+ isPressed: PropTypes.bool,
+ isSignedIn: PropTypes.bool,
+ currentQuestion: PropTypes.number,
+ shouldShakeQuestion: PropTypes.bool
+ };
+
+ handleMouseUp(e, answer, info) {
+ e.stopPropagation();
+ if (!this.props.isPressed) {
+ return null;
+ }
+
+ const {
+ releaseQuestion,
+ answerQuestion
+ } = this.props;
+
+ releaseQuestion();
+ answerQuestion({
+ e,
+ answer,
+ info,
+ threshold: answerThreshold
+ });
+ }
+
+ handleMouseMove(isPressed, { delta, moveQuestion }) {
+ if (!isPressed) {
+ return null;
+ }
+ return e => moveQuestion({ e, delta });
+ }
+
+ onAnswer(answer, userAnswer, info) {
+ const { isSignedIn, answerQuestion } = this.props;
+ return e => {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+
+ return answerQuestion({
+ answer,
+ userAnswer,
+ info,
+ isSignedIn
+ });
+ };
+ }
+
+ renderQuestion(number, question, answer, shouldShakeQuestion, info) {
+ const { grabQuestion, isPressed } = this.props;
+ const mouseUp = e => this.handleMouseUp(e, answer, info);
+ return ({ x }) => {
+ const style = {
+ WebkitTransform: `translate3d(${ x }px, 0, 0)`,
+ transform: `translate3d(${ x }px, 0, 0)`
+ };
+ return (
+
+ Question { number }
+ { question }
+
+ );
+ };
+ }
+
+ render() {
+ const {
+ tests = [],
+ mouse: [x],
+ currentQuestion,
+ shouldShakeQuestion
+ } = this.props;
+
+ const [ question, answer, info ] = tests[currentQuestion - 1] || [];
+ const questionElement = this.renderQuestion(
+ currentQuestion,
+ question,
+ answer,
+ shouldShakeQuestion,
+ info
+ );
+
+ return (
+
this.handleMouseUp(e, answer, info) }
+ xs={ 8 }
+ xsOffset={ 2 }>
+
+
+ { questionElement }
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default connect(mapStateToProps, actionsToBind)(Question);
diff --git a/common/app/routes/Hikes/redux/actions.js b/common/app/routes/Hikes/redux/actions.js
index a2aec2358a..51eabf90ca 100644
--- a/common/app/routes/Hikes/redux/actions.js
+++ b/common/app/routes/Hikes/redux/actions.js
@@ -43,7 +43,7 @@ export const moveQuestion = createAction(
// info: String,
// threshold: Number
// }) => Action
-export const answer = createAction(types.answer);
+export const answerQuestion = createAction(types.answerQuestion);
export const startShake = createAction(types.startShake);
export const endShake = createAction(types.primeNextQuestion);
diff --git a/common/app/routes/Hikes/redux/answer-saga.js b/common/app/routes/Hikes/redux/answer-saga.js
index 0c4cd39211..c9c2500899 100644
--- a/common/app/routes/Hikes/redux/answer-saga.js
+++ b/common/app/routes/Hikes/redux/answer-saga.js
@@ -10,7 +10,7 @@ import { postJSON$ } from '../../../../utils/ajax-stream';
export default () => ({ getState, dispatch }) => next => {
return function answerSaga(action) {
- if (types.answer !== action.type) {
+ if (types.answerQuestion !== action.type) {
return next(action);
}
@@ -56,14 +56,11 @@ export default () => ({ getState, dispatch }) => next => {
// incorrect question
if (answer !== finalAnswer) {
if (info) {
- dispatch({
- type: 'makeToast',
- payload: {
- title: 'Hint',
- message: info,
- type: 'info'
- }
- });
+ dispatch(makeToast({
+ title: 'Hint',
+ message: info,
+ type: 'info'
+ }));
}
return Observable
@@ -100,7 +97,7 @@ export default () => ({ getState, dispatch }) => next => {
})
.catch(error => {
return Observable.just({
- type: 'error',
+ type: 'app.error',
error
});
});
diff --git a/common/app/routes/Hikes/redux/fetch-hikes-saga.js b/common/app/routes/Hikes/redux/fetch-hikes-saga.js
index 07d482f358..5315a38ae8 100644
--- a/common/app/routes/Hikes/redux/fetch-hikes-saga.js
+++ b/common/app/routes/Hikes/redux/fetch-hikes-saga.js
@@ -32,7 +32,6 @@ export default ({ services }) => ({ dispatch }) => next => {
const currentHike = getCurrentHike(hikes, dashedName);
- console.log('foo', currentHike);
return fetchHikesCompleted(hikes, currentHike);
})
.catch(error => {
diff --git a/common/app/routes/Hikes/redux/reducer.js b/common/app/routes/Hikes/redux/reducer.js
index e6a49f0964..d0a95d7e15 100644
--- a/common/app/routes/Hikes/redux/reducer.js
+++ b/common/app/routes/Hikes/redux/reducer.js
@@ -7,16 +7,27 @@ const initialState = {
results: [],
entities: {}
},
- // lecture state
+ // ui
+ // hike dashedName
currentHike: '',
- showQuestions: false
+ // 1 indexed
+ currentQuestion: 1,
+ // [ xPosition, yPosition ]
+ mouse: [ 0, 0 ],
+ // change in mouse position since pressed
+ // [ xDelta, yDelta ]
+ delta: [ 0, 0 ],
+ isPressed: false,
+ isCorrect: false,
+ shouldShakeQuestion: false,
+ shouldShowQuestions: false
};
export default handleActions(
{
[types.toggleQuestion]: state => ({
...state,
- showQuestions: !state.showQuestions,
+ shouldShowQuestions: !state.shouldShowQuestions,
currentQuestion: 1
}),
@@ -38,13 +49,13 @@ export default handleActions(
[types.resetHike]: state => ({
...state,
currentQuestion: 1,
- showQuestions: false,
+ shouldShowQuestions: false,
mouse: [0, 0],
delta: [0, 0]
}),
- [types.startShake]: state => ({ ...state, shake: true }),
- [types.endShake]: state => ({ ...state, shake: false }),
+ [types.startShake]: state => ({ ...state, shouldShakeQuestion: true }),
+ [types.endShake]: state => ({ ...state, shouldShakeQuestion: false }),
[types.primeNextQuestion]: (state, { payload: userAnswer }) => ({
...state,
@@ -68,7 +79,7 @@ export default handleActions(
[types.goToNextHike]: state => ({
...state,
- currentHike: findNextHike(state.hikes, state.currentHike.id),
+ currentHike: findNextHike(state.hikes, state.currentHike),
showQuestions: false,
currentQuestion: 1,
mouse: [ 0, 0 ]
diff --git a/common/app/routes/Hikes/redux/types.js b/common/app/routes/Hikes/redux/types.js
index c96ae0e8e5..d04ab1c359 100644
--- a/common/app/routes/Hikes/redux/types.js
+++ b/common/app/routes/Hikes/redux/types.js
@@ -7,7 +7,7 @@ const types = [
'releaseQuestion',
'moveQuestion',
- 'answer',
+ 'answerQuestion',
'startShake',
'endShake',
diff --git a/common/app/utils/professor-x.js b/common/app/utils/professor-x.js
index ca1522ac0c..d1b2512858 100644
--- a/common/app/utils/professor-x.js
+++ b/common/app/utils/professor-x.js
@@ -1,5 +1,6 @@
import React, { PropTypes, createElement } from 'react';
import { Observable, CompositeDisposable } from 'rx';
+import shouldComponentUpdate from 'react-pure-render/function';
import debug from 'debug';
// interface contain {
@@ -179,10 +180,7 @@ export default function contain(options = {}, Component) {
}
}
- shouldComponentUpdate() {
- // props should be immutable
- return false;
- }
+ shouldComponentUpdate = shouldComponentUpdate;
render() {
const { props } = this;
diff --git a/server/services/hikes.js b/server/services/hikes.js
index e672d857f3..456cd80f4a 100644
--- a/server/services/hikes.js
+++ b/server/services/hikes.js
@@ -25,7 +25,7 @@ export default function hikesService(app) {
if (err) {
return cb(err);
}
- cb(null, hikes);
+ cb(null, hikes.map(hike => hike.toJSON()));
});
}
};