From 4391bfc60bc1b35508a7215cc8be79d0b6141e69 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 20 Dec 2015 19:24:38 -0800 Subject: [PATCH 01/31] Move hikes store to main store --- common/app/flux/Store.js | 11 ++++++++++- common/app/routes/Hikes/flux/Actions.js | 25 +++++++++++++++++-------- common/app/routes/Hikes/flux/Store.js | 20 -------------------- 3 files changed, 27 insertions(+), 29 deletions(-) delete mode 100644 common/app/routes/Hikes/flux/Store.js diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index 33741a1165..f174314f5a 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -5,7 +5,11 @@ const initValue = { title: 'Learn To Code | Free Code Camp', username: null, picture: null, - points: 0 + points: 0, + hikesApp: { + hikes: [], + currentHikes: {} + } }; export default Store({ @@ -16,9 +20,14 @@ export default Store({ init({ instance: appStore, args: [cat] }) { const { updateRoute, setUser, setTitle } = cat.getActions('appActions'); const register = createRegistrar(appStore); + let { setHikes } = cat.getActions('hikesActions'); + // app register(setter(fromMany(setUser, setTitle, updateRoute))); + // hikes + register(setHikes); + return appStore; } }); diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index f6ed407fae..3eed55159f 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -37,25 +37,34 @@ export default Actions({ ({ isPrimed, dashedName }) => { if (isPrimed) { return hikeActions.setHikes({ - transform: (oldState) => { - const { hikes } = oldState; + transform: (state) => { + + const { hikesApp: oldState } = state; const currentHike = getCurrentHike( - hikes, + oldState.hikes, dashedName, oldState.currentHike ); - return Object.assign({}, oldState, { currentHike }); + + const hikesApp = { ...oldState, currentHike }; + return Object.assign({}, state, { hikesApp }); } }); } + services.read('hikes', null, null, (err, hikes) => { if (err) { - debug('an error occurred fetching hikes', err); + return console.error(err); } + + const hikesApp = { + hikes, + currentHike: getCurrentHike(hikes, dashedName) + }; + hikeActions.setHikes({ - set: { - hikes: hikes, - currentHike: getCurrentHike(hikes, dashedName) + transform(oldState) { + return Object.assign({}, oldState, { hikesApp }); } }); }); diff --git a/common/app/routes/Hikes/flux/Store.js b/common/app/routes/Hikes/flux/Store.js deleted file mode 100644 index 9755ed679a..0000000000 --- a/common/app/routes/Hikes/flux/Store.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Store } from 'thundercats'; - -const initialValue = { - hikes: [], - currentHike: {} -}; - -export default Store({ - refs: { - displayName: 'HikesStore', - value: initialValue - }, - init({ instance: hikeStore, args: [cat] }) { - - let { setHikes } = cat.getActions('hikesActions'); - hikeStore.register(setHikes); - - return hikeStore; - } -}); From 17993226613db516468b6a4a9a46737e5ddce59e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:21:39 -0800 Subject: [PATCH 02/31] Fix hikes seed order --- seed/challenges/hikes/computer-basics.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/hikes/computer-basics.json b/seed/challenges/hikes/computer-basics.json index 7172c145c6..dd7aaf11ef 100644 --- a/seed/challenges/hikes/computer-basics.json +++ b/seed/challenges/hikes/computer-basics.json @@ -1,6 +1,6 @@ { "name": "Computer Basics", - "order": 0.050, + "order": 0, "time": "3h", "challenges": [ { From cec243b032f519af8efeba1abd59110d1de7a3f0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:28:07 -0800 Subject: [PATCH 03/31] [add] Upgrade ThunderCats react --- client/index.js | 4 ++-- package.json | 5 +++-- server/boot/a-react.js | 35 +++++++++++++++++++---------------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/client/index.js b/client/index.js index d76e2a78ef..bf8fe8cb79 100644 --- a/client/index.js +++ b/client/index.js @@ -6,7 +6,7 @@ import debugFactory from 'debug'; import { Router } from 'react-router'; import { createLocation, createHistory } from 'history'; import { hydrate } from 'thundercats'; -import { Render } from 'thundercats-react'; +import { render$ } from 'thundercats-react'; import { app$ } from '../common/app'; @@ -72,7 +72,7 @@ app$({ history, location: appLocation }) }) .flatMap(({ props, appCat }) => { props.history = history; - return Render( + return render$( appCat, React.createElement(Router, props), DOMContianer diff --git a/package.json b/package.json index 116ea1f358..f4de269e23 100644 --- a/package.json +++ b/package.json @@ -115,9 +115,10 @@ "rx": "^4.0.0", "sanitize-html": "^1.11.1", "sort-keys": "^1.1.1", + "stampit": "^2.1.1", "store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server", - "thundercats": "^3.0.0", - "thundercats-react": "~0.4.0", + "thundercats": "^3.1.0", + "thundercats-react": "~0.5.1", "twit": "^2.1.1", "uglify-js": "^2.5.0", "url-regex": "^3.0.0", diff --git a/server/boot/a-react.js b/server/boot/a-react.js index d4a000067f..91e4e8dbe7 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -3,8 +3,10 @@ import { RoutingContext } from 'react-router'; import Fetchr from 'fetchr'; import { createLocation } from 'history'; import debugFactory from 'debug'; +import { dehydrate } from 'thundercats'; +import { renderToString$ } from 'thundercats-react'; + import { app$ } from '../../common/app'; -import { RenderToString } from 'thundercats-react'; const debug = debugFactory('freecc:react-server'); @@ -12,14 +14,13 @@ const debug = debugFactory('freecc:react-server'); // remove their individual controllers const routes = [ '/jobs', - '/jobs/*' -]; - -const devRoutes = [ + '/jobs/*', '/hikes', '/hikes/*' ]; +const devRoutes = []; + export default function reactSubRouter(app) { var router = app.loopback.Router(); @@ -51,20 +52,22 @@ export default function reactSubRouter(app) { return !!props; }) .flatMap(function({ props, AppCat }) { - // call thundercats renderToString - // prefetches data and sets up it up for current state - debug('rendering to string'); - return RenderToString( - AppCat(null, services), + const cat = AppCat(null, services); + debug('render react markup and pre-fetch data'); + return renderToString$( + cat, React.createElement(RoutingContext, props) - ); + ) + .flatMap( + dehydrate(cat), + ({ markup }, data) => ({ markup, data, cat }) + ); }) - // makes sure we only get one onNext and closes subscription - .flatMap(function({ data, markup }) { - debug('react rendered'); + .flatMap(function({ data, markup, cat }) { + debug('react markup rendered, data fetched'); + cat.dispose(); const { title } = data.AppStore; res.expose(data, 'data'); - // now render jade file with markup injected from react return res.render$( 'layout-react', { markup, title } @@ -72,7 +75,7 @@ export default function reactSubRouter(app) { }) .subscribe( function(markup) { - debug('jade rendered'); + debug('html rendered and ready to send'); res.send(markup); }, next From 3028c891d76028b0e78318c66e2e937b4fe03830 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:28:40 -0800 Subject: [PATCH 04/31] Fix hikes order --- server/services/hikes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/hikes.js b/server/services/hikes.js index d8344fe91a..bd4a6810e1 100644 --- a/server/services/hikes.js +++ b/server/services/hikes.js @@ -11,7 +11,7 @@ export default function hikesService(app) { read: (req, resource, params, config, cb) => { const query = { where: { challengeType: '6' }, - order: 'suborder ASC' + order: ['order ASC', 'suborder ASC' ] }; debug('params', params); From 65b6715f68ad90b8dbbc5f86e24f84b46dbb5364 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:32:12 -0800 Subject: [PATCH 05/31] Add services stamp --- common/app/Cat.js | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/common/app/Cat.js b/common/app/Cat.js index 31dae9d294..fb442e3db6 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -1,17 +1,40 @@ import { Cat } from 'thundercats'; +import stamp from 'stampit'; +import { Disposable, Observable } from 'rx'; import { AppActions, AppStore } from './flux'; -import { HikesActions, HikesStore } from './routes/Hikes/flux'; +import { HikesActions } from './routes/Hikes/flux'; import { JobActions, JobsStore} from './routes/Jobs/flux'; -export default Cat() - .init(({ instance: cat, args: [services] }) => { - cat.register(AppActions, null, services); - cat.register(AppStore, null, cat); +export default Cat().init(({ instance: cat, args: [services] }) => { + const serviceStamp = stamp({ + methods: { + readService$(resource, params, config) { - cat.register(HikesActions, null, services); - cat.register(HikesStore, null, cat); + return Observable.create(function(observer) { + services.read(resource, params, config, (err, res) => { + if (err) { + observer.onError(err); + return observer.onCompleted(); + } - cat.register(JobActions, null, cat, services); - cat.register(JobsStore, null, cat); + observer.onNext(res); + observer.onCompleted(); + }); + + return Disposable.create(function() { + observer.onCompleted(); + }); + }); + } + } }); + + cat.register(HikesActions.compose(serviceStamp), null, services); + cat.register(AppActions.compose(serviceStamp), null, services); + cat.register(AppStore, null, cat); + + + cat.register(JobActions, null, cat, services); + cat.register(JobsStore.compose(serviceStamp), null, cat); +}); From 22cb610279ff17eac364eeb5f73b8e35ee0588d5 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:33:25 -0800 Subject: [PATCH 06/31] Make structure changes to hikes --- common/app/flux/Actions.js | 64 ++++++++-------- common/app/flux/Store.js | 8 +- common/app/routes/Hikes/components/Hike.jsx | 56 ++++++++++++++ common/app/routes/Hikes/components/Hikes.jsx | 13 +++- .../app/routes/Hikes/components/Lecture.jsx | 36 +++------ common/app/routes/Hikes/components/Map.jsx | 2 +- .../{Question.jsx => Questions.jsx} | 2 +- common/app/routes/Hikes/flux/Actions.js | 74 +++++++++---------- common/app/routes/Hikes/flux/index.js | 1 - common/app/routes/Hikes/index.js | 8 +- server/middlewares/csp.js | 6 +- 11 files changed, 154 insertions(+), 116 deletions(-) create mode 100644 common/app/routes/Hikes/components/Hike.jsx rename common/app/routes/Hikes/components/{Question.jsx => Questions.jsx} (99%) diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 52334c4876..83196b90f0 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -4,46 +4,42 @@ import debugFactory from 'debug'; const debug = debugFactory('freecc:app:actions'); export default Actions({ + shouldBindMethods: true, + refs: { displayName: 'AppActions' }, + setTitle(title = 'Learn To Code') { - return { title: title + '| Free Code Camp' }; + return { title: title + ' | Free Code Camp' }; }, - setUser({ - username, - picture, - progressTimestamps = [], - isFrontEndCert, - isFullStackCert - }) { - return { - username, - picture, - points: progressTimestamps.length, - isFrontEndCert, - isFullStackCert - }; + getUser({ isPrimed }) { + if (isPrimed) { + return null; + } + + debug('fetching user data'); + return this.readService$('user', null, null) + .map(function({ + username, + picture, + progressTimestamps = [], + isFrontEndCert, + isFullStackCert + }) { + return { + username, + picture, + points: progressTimestamps.length, + isFrontEndCert, + isFullStackCert + }; + }) + .catch(err => { + console.error(err); + }); }, - getUser: null, updateRoute(route) { return { route }; }, goBack: null -}) - .refs({ displayName: 'AppActions' }) - .init(({ instance: appActions, args: [services] }) => { - appActions.getUser.subscribe(({ isPrimed }) => { - if (isPrimed) { - debug('isPrimed'); - return; - } - services.read('user', null, null, (err, user) => { - if (err) { - return debug('user service error'); - } - debug('user service returned successful'); - return appActions.setUser(user); - }); - }); - return appActions; - }); +}); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index f174314f5a..59f2f7e959 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -18,15 +18,15 @@ export default Store({ value: initValue }, init({ instance: appStore, args: [cat] }) { - const { updateRoute, setUser, setTitle } = cat.getActions('appActions'); + const { updateRoute, getUser, setTitle } = cat.getActions('appActions'); const register = createRegistrar(appStore); - let { setHikes } = cat.getActions('hikesActions'); + const { fetchHikes } = cat.getActions('hikesActions'); // app - register(setter(fromMany(setUser, setTitle, updateRoute))); + register(setter(fromMany(getUser, setTitle, updateRoute))); // hikes - register(setHikes); + register(fetchHikes); return appStore; } diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/Hikes/components/Hike.jsx new file mode 100644 index 0000000000..dbf00314d8 --- /dev/null +++ b/common/app/routes/Hikes/components/Hike.jsx @@ -0,0 +1,56 @@ +import React, { PropTypes } from 'react'; +import { + Col, + Panel, + Row +} from 'react-bootstrap'; + +import Lecture from './Lecture.jsx'; +import Questions from './Questions.jsx'; + +export default React.createClass({ + displayName: 'Hike', + + propTypes: { + showQuestions: PropTypes.bool, + currentHike: PropTypes.object + }, + + renderBody(showQuestions, currentHike) { + if (showQuestions) { + return ( + + ); + } + + const { + challengeSeed: [ id ] = ['1'], + description = [] + } = currentHike; + + return ( + + ); + }, + + render() { + const { currentHike, showQuestions } = this.props; + const { title } = currentHike; + + const videoTitle =

{ title }

; + + return ( + + + + { this.renderBody(showQuestions, currentHike) } + + + + ); + } +}); diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx index 76632a1456..7943b49320 100644 --- a/common/app/routes/Hikes/components/Hikes.jsx +++ b/common/app/routes/Hikes/components/Hikes.jsx @@ -9,7 +9,10 @@ import HikesMap from './Map.jsx'; export default contain( { - store: 'hikesStore', + store: 'appStore', + map(state) { + return state.hikesApp; + }, actions: ['appActions'], fetchAction: 'hikesActions.fetchHikes', getPayload: ({ hikes, params }) => ({ @@ -54,8 +57,12 @@ export default contain( return (
- { this.renderChild(children, hikes, currentHike) || - this.renderMap(hikes) } + { + // render sub-route + this.renderChild(children, hikes, currentHike) || + // if no sub-route render hikes map + this.renderMap(hikes) + }
); diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx index e47dc0fc98..b70c50d250 100644 --- a/common/app/routes/Hikes/components/Lecture.jsx +++ b/common/app/routes/Hikes/components/Lecture.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Button, Col, Row, Panel } from 'react-bootstrap'; +import { Button, Col, Row } from 'react-bootstrap'; import { History } from 'react-router'; import Vimeo from 'react-vimeo'; import debugFactory from 'debug'; @@ -19,8 +19,6 @@ export default React.createClass({ handleFinish() { debug('loading questions'); - const { dashedName } = this.props.params; - this.history.pushState(null, `/hikes/${dashedName}/questions/1`); }, renderTranscript(transcript, dashedName) { @@ -31,7 +29,6 @@ export default React.createClass({ render() { const { - title, challengeSeed = ['1'], description = [] } = this.props.currentHike; @@ -39,31 +36,22 @@ export default React.createClass({ const [ id ] = challengeSeed; - const videoTitle =

{ title }

; return ( - - - + - - - { this.renderTranscript(description, dashedName) } - - - - - + { this.renderTranscript(description, dashedName) } + ); diff --git a/common/app/routes/Hikes/components/Map.jsx b/common/app/routes/Hikes/components/Map.jsx index 830f74a7ef..64e837603b 100644 --- a/common/app/routes/Hikes/components/Map.jsx +++ b/common/app/routes/Hikes/components/Map.jsx @@ -11,7 +11,7 @@ export default React.createClass({ render() { const { - hikes + hikes = [{}] } = this.props; const vidElements = hikes.map(({ title, dashedName}) => { diff --git a/common/app/routes/Hikes/components/Question.jsx b/common/app/routes/Hikes/components/Questions.jsx similarity index 99% rename from common/app/routes/Hikes/components/Question.jsx rename to common/app/routes/Hikes/components/Questions.jsx index 63dd15407d..d426b14916 100644 --- a/common/app/routes/Hikes/components/Question.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -16,7 +16,7 @@ const debug = debugFactory('freecc:hikes'); const ANSWER_THRESHOLD = 200; export default React.createClass({ - displayName: 'Question', + displayName: 'Questions', mixins: [ History, diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 3eed55159f..3f5e262f31 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -25,49 +25,41 @@ function getCurrentHike(hikes = [{}], dashedName, currentHike) { } export default Actions({ - // start fetching hikes - fetchHikes: null, - // set hikes on store - setHikes: null -}) - .refs({ displayName: 'HikesActions' }) - .init(({ instance: hikeActions, args: [services] }) => { - // set up hikes fetching - hikeActions.fetchHikes.subscribe( - ({ isPrimed, dashedName }) => { - if (isPrimed) { - return hikeActions.setHikes({ - transform: (state) => { + refs: { displayName: 'HikesActions' }, + shouldBindMethods: true, + fetchHikes({ isPrimed, dashedName }) { + if (isPrimed) { + return { + transform: (state) => { - const { hikesApp: oldState } = state; - const currentHike = getCurrentHike( - oldState.hikes, - dashedName, - oldState.currentHike - ); + const { hikesApp: oldState } = state; + const currentHike = getCurrentHike( + oldState.hikes, + dashedName, + oldState.currentHike + ); - const hikesApp = { ...oldState, currentHike }; - return Object.assign({}, state, { hikesApp }); - } - }); + const hikesApp = { ...oldState, currentHike }; + return Object.assign({}, state, { hikesApp }); } + }; + } - services.read('hikes', null, null, (err, hikes) => { - if (err) { - return console.error(err); + return this.readService$('hikes', null, null) + .map(hikes => { + const hikesApp = { + hikes, + currentHike: getCurrentHike(hikes, dashedName) + }; + + return { + transform(oldState) { + return Object.assign({}, oldState, { hikesApp }); } - - const hikesApp = { - hikes, - currentHike: getCurrentHike(hikes, dashedName) - }; - - hikeActions.setHikes({ - transform(oldState) { - return Object.assign({}, oldState, { hikesApp }); - } - }); - }); - } - ); - }); + }; + }) + .catch(err => { + console.error(err); + }); + } +}); diff --git a/common/app/routes/Hikes/flux/index.js b/common/app/routes/Hikes/flux/index.js index 05980cb3ff..336f72d297 100644 --- a/common/app/routes/Hikes/flux/index.js +++ b/common/app/routes/Hikes/flux/index.js @@ -1,2 +1 @@ export { default as HikesActions } from './Actions'; -export { default as HikesStore } from './Store'; diff --git a/common/app/routes/Hikes/index.js b/common/app/routes/Hikes/index.js index 203380153e..a53ac3193d 100644 --- a/common/app/routes/Hikes/index.js +++ b/common/app/routes/Hikes/index.js @@ -1,6 +1,5 @@ import Hikes from './components/Hikes.jsx'; -import Lecture from './components/Lecture.jsx'; -import Question from './components/Question.jsx'; +import Hike from './components/Hike.jsx'; /* * show video /hikes/someVideo @@ -12,9 +11,6 @@ export default { component: Hikes, childRoutes: [{ path: ':dashedName', - component: Lecture - }, { - path: ':dashedName/questions/:number', - component: Question + component: Hike }] }; diff --git a/server/middlewares/csp.js b/server/middlewares/csp.js index 80b41e5fbe..21e542dd01 100644 --- a/server/middlewares/csp.js +++ b/server/middlewares/csp.js @@ -1,9 +1,13 @@ import helmet from 'helmet'; -const trusted = [ +let trusted = [ "'self'" ]; +if (process.env.NODE_ENV !== 'production') { + trusted.push('ws://localhost:3001'); +} + export default function csp() { return helmet.csp({ defaultSrc: trusted, From 935760a84a5b79a84b789fb55dbd1a63c61da4fc Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:59:27 -0800 Subject: [PATCH 07/31] [fix] Lecture loads --- common/app/routes/Hikes/components/Hike.jsx | 16 +++++++++++----- common/app/routes/Hikes/components/Hikes.jsx | 10 ++++++---- common/app/routes/Hikes/components/Lecture.jsx | 13 ++++++------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/Hikes/components/Hike.jsx index dbf00314d8..de5367ed09 100644 --- a/common/app/routes/Hikes/components/Hike.jsx +++ b/common/app/routes/Hikes/components/Hike.jsx @@ -12,11 +12,12 @@ export default React.createClass({ displayName: 'Hike', propTypes: { - showQuestions: PropTypes.bool, - currentHike: PropTypes.object + dashedName: PropTypes.string, + currentHike: PropTypes.object, + showQuestions: PropTypes.bool }, - renderBody(showQuestions, currentHike) { + renderBody(showQuestions, currentHike, dashedName) { if (showQuestions) { return ( @@ -30,13 +31,18 @@ export default React.createClass({ return ( ); }, render() { - const { currentHike, showQuestions } = this.props; + const { + currentHike = {}, + dashedName, + showQuestions + } = this.props; const { title } = currentHike; const videoTitle =

{ title }

; @@ -47,7 +53,7 @@ export default React.createClass({ - { this.renderBody(showQuestions, currentHike) } + { this.renderBody(showQuestions, currentHike, dashedName) } diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx index 7943b49320..33cf820666 100644 --- a/common/app/routes/Hikes/components/Hikes.jsx +++ b/common/app/routes/Hikes/components/Hikes.jsx @@ -30,7 +30,8 @@ export default contain( appActions: PropTypes.object, children: PropTypes.element, currentHike: PropTypes.object, - hikes: PropTypes.array + hikes: PropTypes.array, + params: PropTypes.object }, componentWillMount() { @@ -44,22 +45,23 @@ export default contain( ); }, - renderChild(children, hikes, currentHike) { + renderChild(children, hikes, currentHike, dashedName) { if (!children) { return null; } - return React.cloneElement(children, { hikes, currentHike }); + return React.cloneElement(children, { hikes, currentHike, dashedName }); }, render() { const { hikes, children, currentHike } = this.props; + const { dashedName } = this.props.params; const preventOverflow = { overflow: 'hidden' }; return (
{ // render sub-route - this.renderChild(children, hikes, currentHike) || + this.renderChild(children, hikes, currentHike, dashedName) || // if no sub-route render hikes map this.renderMap(hikes) } diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx index b70c50d250..86ab935c6f 100644 --- a/common/app/routes/Hikes/components/Lecture.jsx +++ b/common/app/routes/Hikes/components/Lecture.jsx @@ -11,8 +11,9 @@ export default React.createClass({ mixins: [History], propTypes: { - currentHike: PropTypes.object, - params: PropTypes.object + dashedName: PropTypes.string, + description: PropTypes.array, + id: PropTypes.string }, handleError: debug, @@ -29,12 +30,10 @@ export default React.createClass({ render() { const { - challengeSeed = ['1'], + id = '1', + dashedName, description = [] - } = this.props.currentHike; - const { dashedName } = this.props.params; - - const [ id ] = challengeSeed; + } = this.props; return ( From 205b0da43f3dfeb644e8ffaef8ddecfd34db16cb Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 26 Dec 2015 00:42:39 -0800 Subject: [PATCH 08/31] First question loads --- common/app/Cat.js | 9 +- common/app/flux/Store.js | 8 +- common/app/routes/Hikes/components/Hike.jsx | 26 +- common/app/routes/Hikes/components/Hikes.jsx | 11 +- .../app/routes/Hikes/components/Lecture.jsx | 116 +++-- .../app/routes/Hikes/components/Questions.jsx | 469 +++++++++--------- common/app/routes/Hikes/flux/Actions.js | 59 ++- 7 files changed, 374 insertions(+), 324 deletions(-) diff --git a/common/app/Cat.js b/common/app/Cat.js index fb442e3db6..2953c8aa5f 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -2,10 +2,17 @@ import { Cat } from 'thundercats'; import stamp from 'stampit'; import { Disposable, Observable } from 'rx'; +import { 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$ + } +}); + export default Cat().init(({ instance: cat, args: [services] }) => { const serviceStamp = stamp({ methods: { @@ -30,7 +37,7 @@ export default Cat().init(({ instance: cat, args: [services] }) => { } }); - cat.register(HikesActions.compose(serviceStamp), null, services); + cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services); cat.register(AppActions.compose(serviceStamp), null, services); cat.register(AppStore, null, cat); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index 59f2f7e959..b052365a92 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -8,7 +8,9 @@ const initValue = { points: 0, hikesApp: { hikes: [], - currentHikes: {} + currentHikes: {}, + currentQuestion: 1, + showQuestion: false } }; @@ -20,13 +22,13 @@ export default Store({ init({ instance: appStore, args: [cat] }) { const { updateRoute, getUser, setTitle } = cat.getActions('appActions'); const register = createRegistrar(appStore); - const { fetchHikes } = cat.getActions('hikesActions'); + const { toggleQuestions, fetchHikes } = cat.getActions('hikesActions'); // app register(setter(fromMany(getUser, setTitle, updateRoute))); // hikes - register(fetchHikes); + register(fromMany(fetchHikes, toggleQuestions)); return appStore; } diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/Hikes/components/Hike.jsx index de5367ed09..7edb724a92 100644 --- a/common/app/routes/Hikes/components/Hike.jsx +++ b/common/app/routes/Hikes/components/Hike.jsx @@ -12,38 +12,22 @@ export default React.createClass({ displayName: 'Hike', propTypes: { - dashedName: PropTypes.string, currentHike: PropTypes.object, showQuestions: PropTypes.bool }, - renderBody(showQuestions, currentHike, dashedName) { + renderBody(showQuestions) { if (showQuestions) { - return ( - - ); + return ; } - - const { - challengeSeed: [ id ] = ['1'], - description = [] - } = currentHike; - - return ( - - ); + return ; }, render() { const { - currentHike = {}, - dashedName, + currentHike: { title } = {}, showQuestions } = this.props; - const { title } = currentHike; const videoTitle =

{ title }

; @@ -53,7 +37,7 @@ export default React.createClass({ - { this.renderBody(showQuestions, currentHike, dashedName) } + { this.renderBody(showQuestions) }
diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx index 33cf820666..95aef149fd 100644 --- a/common/app/routes/Hikes/components/Hikes.jsx +++ b/common/app/routes/Hikes/components/Hikes.jsx @@ -31,7 +31,8 @@ export default contain( children: PropTypes.element, currentHike: PropTypes.object, hikes: PropTypes.array, - params: PropTypes.object + params: PropTypes.object, + showQuestions: PropTypes.bool }, componentWillMount() { @@ -45,15 +46,15 @@ export default contain( ); }, - renderChild(children, hikes, currentHike, dashedName) { + renderChild({ children, ...props }) { if (!children) { return null; } - return React.cloneElement(children, { hikes, currentHike, dashedName }); + return React.cloneElement(children, props); }, render() { - const { hikes, children, currentHike } = this.props; + const { hikes } = this.props; const { dashedName } = this.props.params; const preventOverflow = { overflow: 'hidden' }; return ( @@ -61,7 +62,7 @@ export default contain( { // render sub-route - this.renderChild(children, hikes, currentHike, dashedName) || + this.renderChild({ ...this.props, dashedName }) || // if no sub-route render hikes map this.renderMap(hikes) } diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx index 86ab935c6f..be26aa540a 100644 --- a/common/app/routes/Hikes/components/Lecture.jsx +++ b/common/app/routes/Hikes/components/Lecture.jsx @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import { contain } from 'thundercats-react'; import { Button, Col, Row } from 'react-bootstrap'; import { History } from 'react-router'; import Vimeo from 'react-vimeo'; @@ -6,53 +7,82 @@ import debugFactory from 'debug'; const debug = debugFactory('freecc:hikes'); -export default React.createClass({ - displayName: 'Lecture', - mixins: [History], +export default contain( + { + actions: ['hikesActions'], + store: 'appStore', + map(state) { + const { + currentHike: { + dashedName, + description, + challengeSeed: [id] = [0] + } = {} + } = state.hikesApp; - propTypes: { - dashedName: PropTypes.string, - description: PropTypes.array, - id: PropTypes.string + return { + dashedName, + description, + id + }; + } }, + React.createClass({ + displayName: 'Lecture', + mixins: [History], - handleError: debug, + propTypes: { + dashedName: PropTypes.string, + description: PropTypes.array, + id: PropTypes.string, + hikesActions: PropTypes.object + }, - handleFinish() { - debug('loading questions'); - }, + shouldComponentUpdate(nextProps) { + const { props } = this; + return nextProps.id !== props.id; + }, - renderTranscript(transcript, dashedName) { - return transcript.map((line, index) => ( -

{ line }

- )); - }, + handleError: debug, - render() { - const { - id = '1', - dashedName, - description = [] - } = this.props; + handleFinish(hikesActions) { + debug('loading questions'); + hikesActions.toggleQuestions(); + }, - return ( - - - - - - { this.renderTranscript(description, dashedName) } - - - - ); - } -}); + renderTranscript(transcript, dashedName) { + return transcript.map((line, index) => ( +

{ line }

+ )); + }, + + render() { + const { + id = '1', + description = [], + hikesActions + } = this.props; + const dashedName = 'foo'; + + return ( + + + + + + { this.renderTranscript(description, dashedName) } + + + + ); + } + }) +); diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index d426b14916..1a3160ef1e 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { Motion } from 'react-motion'; -import { History, Lifecycle } from 'react-router'; +import { contain } from 'thundercats-react'; import debugFactory from 'debug'; import { Button, @@ -10,269 +10,250 @@ import { Row } from 'react-bootstrap'; -import { postJSON$ } from '../../../../utils/ajax-stream.js'; - const debug = debugFactory('freecc:hikes'); const ANSWER_THRESHOLD = 200; -export default React.createClass({ - displayName: 'Questions', +export default contain( + { + store: 'appStore', + actions: ['hikesAction'], + map(state) { + const { currentQuestion, currentHike } = state.hikesApp; - mixins: [ - History, - Lifecycle - ], - - propTypes: { - currentHike: PropTypes.object, - dashedName: PropTypes.string, - hikes: PropTypes.array, - params: 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]); + return { + hike: currentHike, + currentQuestion + }; } - 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] - }); }, + React.createClass({ + displayName: 'Questions', - handleMouseUp() { - const { correct } = this.state; - if (correct) { - return this.setState({ - isPressed: false, - delta: [0, 0] - }); - } - this.setState({ - isPressed: false, + propTypes: { + dashedName: PropTypes.string, + currentQuestion: PropTypes.number, + hike: PropTypes.object, + hikesActions: PropTypes.object + }, + + getInitialState: () => ({ mouse: [0, 0], - delta: [0, 0] - }); - }, + correct: false, + delta: [0, 0], + isPressed: false, + showInfo: false, + shake: false + }), - handleMouseMove(answer) { - return (e) => { - let { pageX, pageY, touches } = e; + getTweenValues() { + const { mouse: [x, y] } = this.state; + return { + val: { x, y }, + config: [120, 10] + }; + }, + handleMouseDown({ pageX, pageY, touches }) { if (touches) { - e.preventDefault(); - // these reassins 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 }); - } - }; - }, - - hideInfo() { - this.setState({ showInfo: false }); - }, - - onAnswer(answer, userAnswer) { - 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'); + const { mouse: [pressX, pressY] } = this.state; + const dx = pageX - pressX; + const dy = pageY - pressY; this.setState({ - showInfo: true, - shake: true + isPressed: true, + delta: [dx, dy], + mouse: [pageX - dx, pageY - dy] }); + }, - this.disposeTimeout = setTimeout( - () => this.setState({ shake: false }), - 500 - ); - }; - }, - - onCorrectAnswer() { - const { hikes, currentHike } = this.props; - const { dashedName, number } = this.props.params; - const { id, name, difficulty, tests } = currentHike; - const nextQuestionIndex = +number; - - postJSON$('/completed-challenge', { id, name }).subscribeOnCompleted(() => { - if (tests[nextQuestionIndex]) { - return this.history.pushState( - null, - `/hikes/${ dashedName }/questions/${ nextQuestionIndex + 1 }` - ); + handleMouseUp() { + const { correct } = this.state; + if (correct) { + return this.setState({ + isPressed: false, + delta: [0, 0] + }); } - // next questions does not exist; - debug('finding next hike'); - const nextHike = [].slice.call(hikes) - // hikes is in oder of difficulty, lets get reverse order - .reverse() - // now lets find the hike with the difficulty right above this one - .reduce((lowerHike, hike) => { - if (hike.difficulty > difficulty) { - return hike; + this.setState({ + isPressed: false, + mouse: [0, 0], + delta: [0, 0] + }); + }, + + handleMouseMove(answer) { + return (e) => { + let { pageX, pageY, touches } = e; + + if (touches) { + e.preventDefault(); + // these reassins 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)(); } - return lowerHike; - }, null); - - if (nextHike) { - return this.history.pushState(null, `/hikes/${ nextHike.dashedName }`); - } - debug( - 'next Hike was not found, currentHike %s', - currentHike.dashedName - ); - this.history.pushState(null, '/hikes'); - }); - }, - - routerWillLeave(nextState, router, cb) { - // TODO(berks): do animated transitions here stuff here - this.setState({ - showInfo: false, - correct: false, - mouse: [0, 0] - }, cb); - }, - - renderInfo(showInfo, info) { - if (!info) { - return null; - } - return ( - - -

- { info } -

-
- - - -
- ); - }, - - renderQuestion(number, question, answer, shake) { - return ({ x: xFunc }) => { - const x = xFunc().val.x; - const style = { - WebkitTransform: `translate3d(${ x }px, 0, 0)`, - transform: `translate3d(${ x }px, 0, 0)` + if (mouse[0] <= -ANSWER_THRESHOLD) { + this.handleMouseUp(); + return this.onAnswer(answer, false)(); + } + this.setState({ mouse }); + } }; - const title =

Question { number }

; + }, + + hideInfo() { + this.setState({ showInfo: false }); + }, + + onAnswer(answer, userAnswer) { + 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 + ); + }; + }, + + 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, + mouse: [0, 0] + }, cb); + }, + + renderInfo(showInfo, info) { + if (!info) { + return null; + } return ( - -

{ question }

-
+ + +

+ { info } +

+
+ + + +
); - }; - }, + }, - render() { - const { showInfo, shake } = this.state; - const { currentHike: { tests = [] } } = this.props; - const { number = '1' } = this.props.params; - - const [question, answer, info] = tests[number - 1] || []; - - return ( - - - - { this.renderQuestion(number, question, answer, shake) } - - { this.renderInfo(showInfo, info) } - - - + renderQuestion(number, question, answer, shake) { + return ({ x: xFunc }) => { + const x = xFunc().val.x; + const style = { + WebkitTransform: `translate3d(${ x }px, 0, 0)`, + transform: `translate3d(${ x }px, 0, 0)` + }; + const title =

Question { number }

; + return ( + +

{ question }

-
- - ); - } -}); + ); + }; + }, + + render() { + const { showInfo, shake } = this.state; + const { + hike: { tests = [] } = {}, + currentQuestion + } = this.props; + + const [ question, answer, info ] = tests[currentQuestion - 1] || []; + + return ( + + + + { this.renderQuestion(currentQuestion, question, answer, shake) } + + { this.renderInfo(showInfo, info) } + + + + + + + ); + } + }) +); diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 3f5e262f31..56fe187350 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -1,3 +1,5 @@ +import _ from 'lodash'; +import { Observable } from 'rx'; import { Actions } from 'thundercats'; import debugFactory from 'debug'; @@ -24,6 +26,15 @@ function getCurrentHike(hikes = [{}], dashedName, currentHike) { }, currentHike || {}); } +function findNextHike(hikes, id) { + if (!id) { + debug('find next hike no id provided'); + return hikes[0]; + } + const currentIndex = _.findIndex(hikes, ({ id: _id }) => _id === id); + return hikes[currentIndex + 1] || hikes[0]; +} + export default Actions({ refs: { displayName: 'HikesActions' }, shouldBindMethods: true, @@ -47,19 +58,53 @@ export default Actions({ return this.readService$('hikes', null, null) .map(hikes => { - const hikesApp = { - hikes, - currentHike: getCurrentHike(hikes, dashedName) - }; - + const currentHike = getCurrentHike(hikes, dashedName); return { - transform(oldState) { - return Object.assign({}, oldState, { hikesApp }); + transform(state) { + const hikesApp = { ...state.hikesApp, currentHike, hikes }; + return { ...state, hikesApp }; } }; }) .catch(err => { console.error(err); }); + }, + + toggleQuestions() { + return { + transform(state) { + state.hikesApp.showQuestions = !state.hikesApp.showQuestions; + return Object.assign({}, state); + } + }; + }, + + completedHike(data = {}) { + return this.postJSON$('/completed-challenge', data) + .map(() => { + return { + transform(state) { + const { hikes, currentHike: { id } } = state.hikesApp; + const currentHike = findNextHike(hikes, id); + + // go to next route + state.route = currentHike && currentHike.dashedName ? + `/hikes/${ currentHike.dashedName }` : + '/hikes'; + + const hikesApp = { ...state.hikesApp, currentHike }; + return { ...state, hikesApp }; + } + }; + }) + .catch(err => { + console.error(err); + return Observable.just({ + set: { + error: err + } + }); + }); } }); From dd4589dd6f28ce0953eb95a91c6839d4c590d298 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 27 Dec 2015 15:53:48 -0800 Subject: [PATCH 09/31] Fix manifest build order --- gulpfile.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 0e91aaf014..07b0e0426a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -183,7 +183,7 @@ function delRev(dest, manifestName) { }); } -gulp.task('serve', function(cb) { +gulp.task('serve', ['build-manifest'], function(cb) { var called = false; nodemon({ script: paths.server, @@ -481,7 +481,7 @@ function buildManifest() { .pipe(gulp.dest('server/')); } -var buildDependents = ['less', 'js', 'dependents']; +var buildDependents = ['less', 'js', 'dependents', 'pack-watch-manifest']; gulp.task('build-manifest', buildDependents, function() { return buildManifest(); @@ -505,9 +505,9 @@ var watchDependents = [ 'dependents', 'serve', 'sync', - 'build-manifest', 'pack-watch', - 'pack-watch-manifest' + 'pack-watch-manifest', + 'build-manifest' ]; gulp.task('reload', function() { @@ -533,6 +533,7 @@ gulp.task('default', [ 'serve', 'pack-watch', 'pack-watch-manifest', + 'build-manifest-watch', 'watch', 'sync' ]); From acb05e3a715c3fcd08372c908f50530a38eb51db Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 29 Dec 2015 17:35:50 -0800 Subject: [PATCH 10/31] Question now semi functional --- client/index.js | 38 ++-- common/app/Cat.js | 5 +- common/app/flux/Store.js | 26 ++- .../app/routes/Hikes/components/Questions.jsx | 174 +++++++----------- common/app/routes/Hikes/flux/Actions.js | 120 +++++++++++- common/utils/ajax-stream.js | 28 ++- 6 files changed, 252 insertions(+), 139 deletions(-) diff --git a/client/index.js b/client/index.js index bf8fe8cb79..008e2a704b 100644 --- a/client/index.js +++ b/client/index.js @@ -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; diff --git a/common/app/Cat.js b/common/app/Cat.js index 2953c8aa5f..5b2040505e 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -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$ } }); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index b052365a92..e63c76cbc4 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -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; } diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index 1a3160ef1e..e7d4cd9cd2 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -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 (

@@ -184,7 +135,7 @@ export default contain( @@ -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 }> - + { this.renderQuestion(currentQuestion, question, answer, shake) } - { this.renderInfo(showInfo, info) } + { this.renderInfo(showInfo, info, hikesActions.hideInfo) } diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 56fe187350..1078f87e55 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -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({ diff --git a/common/utils/ajax-stream.js b/common/utils/ajax-stream.js index 3bdcc2abdc..ba7b428b2a 100644 --- a/common/utils/ajax-stream.js +++ b/common/utils/ajax-stream.js @@ -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' } From a3a3b1b9f260f0cf2b35a0ec3504b699e995e11e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 14:32:29 -0800 Subject: [PATCH 11/31] Next hike loads up --- common/app/routes/Hikes/flux/Actions.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 1078f87e55..f9928dbb8d 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -163,7 +163,8 @@ export default Actions({ } // move to next question - if (tests[currentQuestion + 1]) { + // index 0 + if (tests[currentQuestion]) { return { transform(state) { @@ -204,7 +205,12 @@ export default Actions({ `/hikes/${ currentHike.dashedName }` : '/hikes'; - const hikesApp = { ...state.hikesApp, currentHike }; + const hikesApp = { + ...state.hikesApp, + currentHike, + showQuestions: false + }; + return { ...state, hikesApp }; } }; From 92246c2dd828f699c3a4eff3bf072db3df13f7f9 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 15:38:21 -0800 Subject: [PATCH 12/31] Fix transition bug --- common/app/routes/Hikes/flux/Actions.js | 44 ++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index f9928dbb8d..18c8c9cd6b 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -136,7 +136,8 @@ export default Actions({ userAnswer, props: { hike: { id, name, tests, challengeType }, - currentQuestion + currentQuestion, + username } }) { @@ -180,6 +181,10 @@ export default Actions({ } // challenge completed + const optimisticSave = username ? + this.post$('/completed-challenge', { id, name, challengeType }) : + Observable.just(true); + const correctAnswer = { transform(state) { const hikesApp = { @@ -193,28 +198,29 @@ export default Actions({ } }; - return this.post$('/completed-challenge', { id, name, challengeType }) - .map(() => { - return { - transform(state) { - const { hikes, currentHike: { id } } = state.hikesApp; - const currentHike = findNextHike(hikes, id); + return Observable.just({ + transform(state) { + const { hikes, currentHike: { id } } = state.hikesApp; + const currentHike = findNextHike(hikes, id); - // go to next route - state.route = currentHike && currentHike.dashedName ? - `/hikes/${ currentHike.dashedName }` : - '/hikes'; + // go to next route + state.route = currentHike && currentHike.dashedName ? + `/hikes/${ currentHike.dashedName }` : + '/hikes'; - const hikesApp = { - ...state.hikesApp, - currentHike, - showQuestions: false - }; + const hikesApp = { + ...state.hikesApp, + currentHike, + showQuestions: false, + currentQuestion: 1, + mouse: [0, 0] + }; - return { ...state, hikesApp }; - } - }; + return { ...state, hikesApp }; + }, + optimistic: optimisticSave }) + .delay(500) .startWith(correctAnswer) .catch(err => { console.error(err); From a39746d381bfe629b3e44a7de9fd90011018c36d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 15:49:44 -0800 Subject: [PATCH 13/31] Fix question motion on correct answer --- common/app/routes/Hikes/flux/Actions.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 18c8c9cd6b..a76a98961a 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -167,17 +167,28 @@ export default Actions({ // index 0 if (tests[currentQuestion]) { - return { + return Observable.just({ transform(state) { - const hikesApp = { ...state.hikesApp, - currentQuestion: currentQuestion + 1 + mouse: [0, 0] }; - return { ...state, hikesApp }; } - }; + }) + .delay(300) + .startWith({ + transform(state) { + + const hikesApp = { + ...state.hikesApp, + currentQuestion: currentQuestion + 1, + mouse: [ userAnswer ? 1000 : -1000, 0] + }; + + return { ...state, hikesApp }; + } + }); } // challenge completed @@ -220,7 +231,7 @@ export default Actions({ }, optimistic: optimisticSave }) - .delay(500) + .delay(300) .startWith(correctAnswer) .catch(err => { console.error(err); From dcf7f59667e03c9c233ec58a80d8a64828e1d049 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 16:14:40 -0800 Subject: [PATCH 14/31] On hike completed, points increase --- common/app/routes/Hikes/components/Questions.jsx | 12 +++++++++--- common/app/routes/Hikes/flux/Actions.js | 11 +++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index e7d4cd9cd2..1a7b08c9e3 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -15,7 +15,7 @@ export default contain( { store: 'appStore', actions: ['hikesActions'], - map({ hikesApp }) { + map({ hikesApp, username }) { const { currentHike, currentQuestion = 1, @@ -34,7 +34,8 @@ export default contain( delta, isPressed, showInfo, - shake + shake, + username }; } }, @@ -50,6 +51,7 @@ export default contain( isPressed: PropTypes.bool, showInfo: PropTypes.bool, shake: PropTypes.bool, + username: PropTypes.string, hikesActions: PropTypes.object }, @@ -104,7 +106,11 @@ export default contain( e.preventDefault(); } - return hikesActions.answer({ answer, userAnswer, props: this.props }); + return hikesActions.answer({ + answer, + userAnswer, + props: this.props + }); }; }, diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index a76a98961a..cc07623d1b 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -205,7 +205,10 @@ export default Actions({ delta: [0, 0], mouse: [ userAnswer ? 1000 : -1000, 0] }; - return { ...state, hikesApp }; + return { + ...state, + hikesApp + }; } }; @@ -227,7 +230,11 @@ export default Actions({ mouse: [0, 0] }; - return { ...state, hikesApp }; + return { + ...state, + points: username ? state.points + 1 : state.points, + hikesApp + }; }, optimistic: optimisticSave }) From 80d36cc3cd13a24665af3203b3253b81cf37ec8e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 31 Dec 2015 17:39:29 -0800 Subject: [PATCH 15/31] Get router history working with flux --- client/index.js | 59 ++++++--------------- client/synchronise-history.js | 69 +++++++++++++++++++++++++ common/app/flux/Actions.js | 14 +++-- common/app/flux/Store.js | 19 ++++++- common/app/routes/Hikes/flux/Actions.js | 9 ++-- 5 files changed, 117 insertions(+), 53 deletions(-) create mode 100644 client/synchronise-history.js diff --git a/client/index.js b/client/index.js index 008e2a704b..5eef4fe032 100644 --- a/client/index.js +++ b/client/index.js @@ -9,6 +9,7 @@ import { hydrate } from 'thundercats'; import { render$ } from 'thundercats-react'; import { app$ } from '../common/app'; +import synchroniseHistory from './synchronise-history'; const debug = debugFactory('fcc:client'); const DOMContianer = document.getElementById('fcc'); @@ -23,18 +24,6 @@ const appLocation = createLocation( location.pathname + location.search ); -function location$(history) { - return Rx.Observable.create(function(observer) { - const dispose = history.listen(function(location) { - observer.onNext(location); - }); - - return Rx.Disposable.create(() => { - dispose(); - }); - }); -} - // returns an observable app$({ history, location: appLocation }) .flatMap( @@ -49,40 +38,22 @@ app$({ history, location: appLocation }) ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) .doOnNext(({ appCat }) => { - const appActions = appCat.getActions('appActions'); - const appStore = appCat.getStore('appStore'); + const { updateLocation, goTo, goBack } = appCat.getActions('appActions'); + const appStore$ = appCat.getStore('appStore'); - const route$ = location$(history) - .pluck('pathname') - .distinctUntilChanged(); + const routerState$ = appStore$ + .map(({ location }) => location) + .distinctUntilChanged( + location => location && location.key ? location.key : location + ); - 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(); - }); - - appActions - .updateRoute - .pluck('route') - .doOnNext(route => { - debug('update route', route); - history.pushState(history.state, route); - }) - .subscribeOnError(err => console.error(err)); + synchroniseHistory( + history, + updateLocation, + goTo, + goBack, + routerState$ + ); }) .flatMap(({ props, appCat }) => { props.history = history; diff --git a/client/synchronise-history.js b/client/synchronise-history.js new file mode 100644 index 0000000000..3e36cf9747 --- /dev/null +++ b/client/synchronise-history.js @@ -0,0 +1,69 @@ +import { Disposable, Observable } from 'rx'; + +export function location$(history) { + return Observable.create(function(observer) { + const dispose = history.listen(function(location) { + observer.onNext(location); + }); + + return Disposable.create(() => { + dispose(); + }); + }); +} + +const emptyLocation = { + pathname: '', + search: '', + hash: '' +}; + +let prevKey; +let isSyncing = false; +export default function synchroniseHistory( + history, + updateLocation, + goTo, + goBack, + routerState$ +) { + routerState$.subscribe( + location => { + + if (!location) { + return null; + } + + // store location has changed, update history + if (location.key !== prevKey) { + isSyncing = true; + history.transitionTo({ ...emptyLocation, ...location }); + isSyncing = false; + } + } + ); + + location$(history) + .doOnNext(location => { + prevKey = location.key; + + if (isSyncing) { + return null; + } + + return updateLocation(location); + }) + .subscribe(() => {}); + + goTo + .doOnNext((route = '/') => { + history.push(route); + }) + .subscribe(() => {}); + + goBack + .doOnNext(() => { + history.goBack(); + }) + .subscribe(() => {}); +} diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 83196b90f0..da2318f56d 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -38,8 +38,14 @@ export default Actions({ }); }, - updateRoute(route) { - return { route }; - }, - goBack: null + // routing + goTo: null, + goBack: null, + updateLocation(location) { + return { + transform(state) { + return { ...state, location }; + } + }; + } }); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index e63c76cbc4..d7dc12e09f 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -20,7 +20,12 @@ export default Store({ value: initValue }, init({ instance: appStore, args: [cat] }) { - const { updateRoute, getUser, setTitle } = cat.getActions('appActions'); + const { + updateLocation, + getUser, + setTitle + } = cat.getActions('appActions'); + const register = createRegistrar(appStore); const { toggleQuestions, @@ -33,7 +38,17 @@ export default Store({ } = cat.getActions('hikesActions'); // app - register(setter(fromMany(getUser, setTitle, updateRoute))); + register( + fromMany( + setter( + fromMany( + getUser, + setTitle + ) + ), + updateLocation + ) + ); // hikes register( diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index cc07623d1b..4556308871 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -218,9 +218,12 @@ export default Actions({ const currentHike = findNextHike(hikes, id); // go to next route - state.route = currentHike && currentHike.dashedName ? - `/hikes/${ currentHike.dashedName }` : - '/hikes'; + state.location = { + action: 'PUSH', + pathname: currentHike && currentHike.dashedName ? + `/hikes/${ currentHike.dashedName }` : + '/hikes' + }; const hikesApp = { ...state.hikesApp, From 30826d9ecb27b8928f240314504cb7a8070990b4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 31 Dec 2015 17:53:16 -0800 Subject: [PATCH 16/31] Fix production build never completing --- gulpfile.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 07b0e0426a..a8e0329ca2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -40,7 +40,7 @@ var Rx = require('rx'), // lint jsonlint = require('gulp-jsonlint'), eslint = require('gulp-eslint'), - + // unit-tests tape = require('gulp-tape'), tapSpec = require('tap-spec'); @@ -481,7 +481,11 @@ function buildManifest() { .pipe(gulp.dest('server/')); } -var buildDependents = ['less', 'js', 'dependents', 'pack-watch-manifest']; +var buildDependents = ['less', 'js', 'dependents']; + +if (__DEV__) { + buildDependents.push('pack-watch-manifest'); +} gulp.task('build-manifest', buildDependents, function() { return buildManifest(); From ea574d721e79acebfee7ab3df053fbed722ddb25 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 3 Jan 2016 19:40:49 -0800 Subject: [PATCH 17/31] Make document titles work --- client/index.js | 9 +++++++++ common/app/App.jsx | 16 ---------------- server/boot/a-react.js | 6 ++++++ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/client/index.js b/client/index.js index 5eef4fe032..e85045329f 100644 --- a/client/index.js +++ b/client/index.js @@ -47,6 +47,12 @@ app$({ history, location: appLocation }) location => location && location.key ? location.key : location ); + // set page title + appStore$ + .pluck('title') + .doOnNext(title => document.title = title) + .subscribe(() => {}); + synchroniseHistory( history, updateLocation, @@ -55,8 +61,11 @@ app$({ history, location: appLocation }) routerState$ ); }) + // allow store subscribe to subscribe to actions + .delay(10) .flatMap(({ props, appCat }) => { props.history = history; + return render$( appCat, React.createElement(Router, props), diff --git a/common/app/App.jsx b/common/app/App.jsx index 25ad3ad91e..1307166924 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -25,22 +25,6 @@ export default contain( username: PropTypes.string }, - componentDidMount() { - const title = this.props.title; - this.setTitle(title); - }, - - componentWillReceiveProps(nextProps) { - if (nextProps.title !== this.props.title) { - this.setTitle(nextProps.title); - } - }, - - setTitle(title) { - const doc = typeof document !== 'undefined' ? document : {}; - doc.title = title; - }, - render() { const { username, points, picture } = this.props; const navProps = { username, points, picture }; diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 91e4e8dbe7..5f094fe973 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -54,6 +54,12 @@ export default function reactSubRouter(app) { .flatMap(function({ props, AppCat }) { const cat = AppCat(null, services); debug('render react markup and pre-fetch data'); + const store = cat.getStore('appStore'); + + // primes store to observe action changes + // cleaned up by cat.dispose further down + store.subscribe(() => {}); + return renderToString$( cat, React.createElement(RoutingContext, props) From b62f09e71a37f246f2163cd2be12693c788b156c Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 4 Jan 2016 14:26:07 -0800 Subject: [PATCH 18/31] Update /jobs --- common/app/Cat.js | 31 +- common/app/flux/Store.js | 61 +++- .../app/routes/Jobs/components/GoToPayPal.jsx | 8 +- common/app/routes/Jobs/components/Jobs.jsx | 15 +- common/app/routes/Jobs/components/NewJob.jsx | 4 +- common/app/routes/Jobs/components/Preview.jsx | 6 +- common/app/routes/Jobs/components/Show.jsx | 23 +- common/app/routes/Jobs/flux/Actions.js | 295 +++++++++--------- common/app/routes/Jobs/flux/Store.js | 42 --- common/app/routes/Jobs/flux/index.js | 1 - common/utils/index.js | 13 + 11 files changed, 258 insertions(+), 241 deletions(-) delete mode 100644 common/app/routes/Jobs/flux/Store.js create mode 100644 common/utils/index.js diff --git a/common/app/Cat.js b/common/app/Cat.js index 5b2040505e..47b31bad6e 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -5,7 +5,7 @@ import { Disposable, Observable } from 'rx'; 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'; +import { JobActions } from './routes/Jobs/flux'; const ajaxStamp = stamp({ methods: { @@ -22,8 +22,7 @@ export default Cat().init(({ instance: cat, args: [services] }) => { return Observable.create(function(observer) { services.read(resource, params, config, (err, res) => { if (err) { - observer.onError(err); - return observer.onCompleted(); + return observer.onError(err); } observer.onNext(res); @@ -31,8 +30,24 @@ export default Cat().init(({ instance: cat, args: [services] }) => { }); return Disposable.create(function() { + observer.dispose(); + }); + }); + }, + createService$(resource, params, body, config) { + return Observable.create(function(observer) { + services.create(resource, params, body, config, (err, res) => { + if (err) { + return observer.onError(err); + } + + observer.onNext(res); observer.onCompleted(); }); + + return Disposable.create(function() { + observer.dispose(); + }); }); } } @@ -40,9 +55,11 @@ export default Cat().init(({ instance: cat, args: [services] }) => { cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services); cat.register(AppActions.compose(serviceStamp), null, services); + cat.register( + JobActions.compose(serviceStamp, ajaxStamp), + null, + cat, + services + ); cat.register(AppStore, null, cat); - - - cat.register(JobActions, null, cat, services); - cat.register(JobsStore.compose(serviceStamp), null, cat); }); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index d7dc12e09f..d995544902 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -11,6 +11,9 @@ const initValue = { // lecture state currentHike: {}, showQuestion: false + }, + jobsApp: { + showModal: false } }; @@ -19,25 +22,15 @@ export default Store({ displayName: 'AppStore', value: initValue }, - init({ instance: appStore, args: [cat] }) { + init({ instance: store, args: [cat] }) { + const register = createRegistrar(store); + // app const { updateLocation, getUser, setTitle } = cat.getActions('appActions'); - const register = createRegistrar(appStore); - const { - toggleQuestions, - fetchHikes, - hideInfo, - grabQuestion, - releaseQuestion, - moveQuestion, - answer - } = cat.getActions('hikesActions'); - - // app register( fromMany( setter( @@ -51,6 +44,16 @@ export default Store({ ); // hikes + const { + toggleQuestions, + fetchHikes, + hideInfo, + grabQuestion, + releaseQuestion, + moveQuestion, + answer + } = cat.getActions('hikesActions'); + register( fromMany( toggleQuestions, @@ -63,6 +66,36 @@ export default Store({ ) ); - return appStore; + + // jobs + const { + findJob, + saveJobToDb, + getJob, + getJobs, + openModal, + closeModal, + handleForm, + getSavedForm, + setPromoCode, + applyCode, + clearPromo + } = cat.getActions('JobActions'); + + register( + fromMany( + findJob, + saveJobToDb, + getJob, + getJobs, + openModal, + closeModal, + handleForm, + getSavedForm, + setPromoCode, + applyCode, + clearPromo + ) + ); } }); diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 8b2710b2a8..cf96c9a1c1 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -11,12 +11,12 @@ const paypalIds = { export default contain( { - store: 'JobsStore', + store: 'appStore', actions: [ 'jobActions', 'appActions' ], - map({ + map({ jobApp: { job: { id, isHighlighted } = {}, buttonId = isHighlighted ? paypalIds.highlighted : @@ -26,7 +26,7 @@ export default contain( promoCode = '', promoApplied = false, promoName - }) { + }}) { return { id, isHighlighted, @@ -57,7 +57,7 @@ export default contain( goToJobBoard() { const { appActions } = this.props; - appActions.updateRoute('/jobs'); + appActions.goTo('/jobs'); }, renderDiscount(discountAmount) { diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 7e23a372f6..b648eeb80f 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -6,7 +6,10 @@ import ListJobs from './List.jsx'; export default contain( { - store: 'jobsStore', + store: 'appStore', + map({ jobsApp: { jobs, showModal }}) { + return { jobs, showModal }; + }, fetchAction: 'jobActions.getJobs', actions: [ 'appActions', @@ -18,25 +21,19 @@ export default contain( propTypes: { children: PropTypes.element, - numOfFollowers: PropTypes.number, appActions: PropTypes.object, jobActions: PropTypes.object, jobs: PropTypes.array, showModal: PropTypes.bool }, - componentDidMount() { - const { jobActions } = this.props; - jobActions.getFollowers(); - }, - handleJobClick(id) { const { appActions, jobActions } = this.props; if (!id) { return null; } jobActions.findJob(id); - appActions.updateRoute(`/jobs/${id}`); + appActions.goTo(`/jobs/${id}`); }, renderList(handleJobClick, jobs) { @@ -84,7 +81,7 @@ export default contain( diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index c482c77094..1eb9178657 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -103,9 +103,9 @@ function makeRequired(validator) { } export default contain({ + store: 'appStore', actions: 'jobActions', - store: 'jobsStore', - map({ form = {} }) { + map({ jobsApp: { form = {} } }) { const { position, locale, diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index c3cb67c170..532d0d2cdd 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -8,12 +8,12 @@ import JobNotFound from './JobNotFound.jsx'; export default contain( { - store: 'JobsStore', + store: 'appStore', actions: [ 'appActions', 'jobActions' ], - map({ form: job = {} }) { + map({ jobApp: { form: job = {} } }) { return { job }; } }, @@ -32,7 +32,7 @@ export default contain( const { appActions, job } = this.props; // redirect user in client if (!job || !job.position || !job.description) { - appActions.updateRoute('/jobs/new'); + appActions.goTo('/jobs/new'); } }, diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 85034e3242..0a2371f91a 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -53,13 +53,14 @@ function generateMessage( export default contain( { - stores: ['appStore', 'jobsStore'], - fetchWaitFor: 'jobsStore', + store: 'appStore', fetchAction: 'jobActions.getJob', - combineLatest( - { username, isFrontEndCert, isFullStackCert }, - { currentJob } - ) { + map({ + username, + isFrontEndCert, + isFullStackCert, + jobsApp: { currentJob } + }) { return { username, job: currentJob, @@ -67,11 +68,11 @@ export default contain( isFullStackCert }; }, - getPayload({ params: { id }, job = {} }) { - return { - id, - isPrimed: job.id === id - }; + getPayload({ params: { id } }) { + return id; + }, + isPrimed({ params: { id } = {}, job = {} }) { + return job.id === id; }, // using es6 destructuring shouldContainerFetch({ job = {} }, { params: { id } } diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 08675048b7..ee50dd419f 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,51 +1,100 @@ import { Actions } from 'thundercats'; import store from 'store'; -import debugFactory from 'debug'; -import { jsonp$ } from '../../../../utils/jsonp$'; -import { postJSON$ } from '../../../../utils/ajax-stream'; +import { nameSpacedTransformer } from '../../../../utils'; -const debug = debugFactory('freecc:jobs:actions'); const assign = Object.assign; +const jobsTranformer = nameSpacedTransformer('jobsApp'); +const noOper = { transform: () => {} }; export default Actions({ - setJobs: null, + refs: { displayName: 'JobActions' }, + shouldBindMethods: true, // findJob assumes that the job is already in the list of jobs findJob(id) { - return oldState => { - const { currentJob = {}, jobs = [] } = oldState; - // currentJob already set - // do nothing - if (currentJob.id === id) { - return null; - } - const foundJob = jobs.reduce((newJob, job) => { - if (job.id === id) { - return job; + return { + transform: jobsTranformer(oldState => { + const { currentJob = {}, jobs = [] } = oldState; + // currentJob already set + // do nothing + if (currentJob.id === id) { + return null; } - return newJob; - }, null); + const foundJob = jobs.reduce((newJob, job) => { + if (job.id === id) { + return job; + } + return newJob; + }, null); - // if no job found this will be null which is a op noop - return foundJob ? - assign({}, oldState, { currentJob: foundJob }) : - null; + // if no job found this will be null which is a op noop + return foundJob ? + assign({}, oldState, { currentJob: foundJob }) : + null; + }) }; }, - setError: null, - getJob: null, - saveJobToDb: null, - getJobs(params) { - return { params }; + saveJobToDb({ goTo, job }) { + return this.createService$('jobs', { job }) + .map(job => ({ + transform(state) { + state.location = { + action: 'PUSH', + pathname: goTo + }; + return { + ...state, + jobs: { + ...state.jobs, + currentJob: job + } + }; + } + })) + .catch(err => ({ + transform(state) { + return { ...state, err }; + } + })); + }, + getJob(id) { + return this.readService$('jobs', { id }) + .map(job => ({ + transform: jobsTranformer(state => { + return { ...state, currentJob: job }; + }) + })) + .catch(err => ({ + transform(state) { + return { ...state, err }; + } + })); + }, + getJobs() { + return this.readService$('jobs') + .map(jobs => ({ + transform: jobsTranformer(state => { + return { ...state, jobs }; + }) + })) + .catch(err => ({ + transform(state) { + return { state, err }; + } + })); }, openModal() { - return { showModal: true }; + return { + transform: jobsTranformer(state => ({ ...state, showModal: true })) + }; }, closeModal() { - return { showModal: false }; + return { + transform: jobsTranformer(state => ({ ...state, showModal: false })) + }; }, handleForm(value) { return { - transform(oldState) { + transform: jobsTranformer(oldState => { const { form } = oldState; const newState = assign({}, oldState); newState.form = assign( @@ -54,142 +103,92 @@ export default Actions({ value ); return newState; - } + }) }; }, saveForm: null, - getSavedForm: null, clearSavedForm: null, - setForm(form) { - return { form }; - }, - getFollowers: null, - setFollowersCount(numOfFollowers) { - return { numOfFollowers }; + getSavedForm() { + const form = store.get('newJob'); + if (form && !Array.isArray(form) && typeof form === 'object') { + return { + transform: jobsTranformer(state => { + return { ...state, form }; + }) + }; + } + return noOper; }, setPromoCode({ target: { value = '' }} = {}) { - return { promoCode: value.replace(/[^\d\w\s]/, '') }; + return { + transform: jobsTranformer(state => ({ + ...state, + promoCode: value.replace(/[^\d\w\s]/, '') + })) + }; + }, + applyCode({ id, code = '', type = null}) { + const body = { + id, + code: code.replace(/[^\d\w\s]/, '') + }; + if (type) { + body.type = type; + } + return this.postJSON$('/api/promos/getButton', body) + .pluck('response') + .map(({ promo }) => { + if (!promo || !promo.buttonId) { + return noOper; + } + const { + fullPrice: price, + buttonId, + discountAmount, + code: promoCode, + name: promoName + } = promo; + + return { + transform: jobsTranformer(state => ({ + ...state, + price, + buttonId, + discountAmount, + promoCode, + promoApplied: true, + promoName + })) + }; + }) + .catch(err => ({ + transform(state) { + return { ...state, err }; + } + })); }, - applyCode: null, clearPromo(foo, undef) { return { - price: undef, - buttonId: undef, - discountAmount: undef, - promoCode: undef, - promoApplied: false, - promoName: undef + transform: jobsTranformer(state => ({ + ...state, + price: undef, + buttonId: undef, + discountAmount: undef, + promoCode: undef, + promoApplied: false, + promoName: undef + })) }; }, - applyPromo({ - fullPrice: price, - buttonId, - discountAmount, - code: promoCode, - name: promoName - } = {}) { - return { - price, - buttonId, - discountAmount, - promoCode, - promoApplied: true, - promoName - }; - } -}) - .refs({ displayName: 'JobActions' }) - .init(({ instance: jobActions, args: [cat, services] }) => { - jobActions.getJobs.subscribe(() => { - services.read('jobs', null, null, (err, jobs) => { - if (err) { - debug('job services experienced an issue', err); - return jobActions.setError({ err }); - } - jobActions.setJobs({ jobs }); - }); - }); - - jobActions.getJob.subscribe(({ id, isPrimed }) => { - // job is already set, do nothing. - if (isPrimed) { - debug('job is primed'); - return; - } - services.read('jobs', { id }, null, (err, job) => { - if (err) { - debug('job services experienced an issue', err); - return jobActions.setError({ err }); - } - if (job) { - jobActions.setJobs({ currentJob: job }); - } - jobActions.setJobs({}); - }); - }); - + init({ instance: jobActions }) { jobActions.saveForm.subscribe((form) => { store.set('newJob', form); }); - jobActions.getSavedForm.subscribe(() => { - const job = store.get('newJob'); - if (job && !Array.isArray(job) && typeof job === 'object') { - jobActions.setForm(job); - } - }); - jobActions.clearSavedForm.subscribe(() => { store.remove('newJob'); }); - jobActions.saveJobToDb.subscribe(({ goTo, job }) => { - const appActions = cat.getActions('appActions'); - services.create('jobs', { job }, null, (err, job) => { - if (err) { - debug('job services experienced an issue', err); - return jobActions.setError(err); - } - jobActions.setJobs({ job }); - appActions.updateRoute(goTo); - }); - }); - - jobActions.getFollowers.subscribe(() => { - const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' + - 'info.json?lang=en&screen_names=CamperJobs' + - '&callback=JSONPCallback'; - - jsonp$(url) - .map(({ response }) => { - return response[0]['followers_count']; - }) - .subscribe( - count => jobActions.setFollowersCount(count), - err => jobActions.setError(err) - ); - }); - - jobActions.applyCode.subscribe(({ id, code = '', type = null}) => { - const body = { - id, - code: code.replace(/[^\d\w\s]/, '') - }; - if (type) { - body.type = type; - } - postJSON$('/api/promos/getButton', body) - .pluck('response') - .subscribe( - ({ promo }) => { - if (promo && promo.buttonId) { - jobActions.applyPromo(promo); - } - jobActions.setError(new Error('no promo found')); - }, - jobActions.setError - ); - }); - return jobActions; - }); + } +}); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js deleted file mode 100644 index b7aa5cda3d..0000000000 --- a/common/app/routes/Jobs/flux/Store.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Store } from 'thundercats'; - -const { - createRegistrar, - setter, - transformer -} = Store; - -export default Store({ - refs: { - displayName: 'JobsStore', - value: { showModal: false } - }, - init({ instance: jobsStore, args: [cat] }) { - const { - setJobs, - findJob, - setError, - openModal, - closeModal, - handleForm, - setForm, - setFollowersCount, - setPromoCode, - applyPromo, - clearPromo - } = cat.getActions('JobActions'); - const register = createRegistrar(jobsStore); - register(setter(setJobs)); - register(setter(setError)); - register(setter(openModal)); - register(setter(closeModal)); - register(setter(setForm)); - register(setter(setPromoCode)); - register(setter(applyPromo)); - register(setter(clearPromo)); - register(setter(setFollowersCount)); - - register(transformer(findJob)); - register(handleForm); - } -}); diff --git a/common/app/routes/Jobs/flux/index.js b/common/app/routes/Jobs/flux/index.js index de123cba0d..61944c2666 100644 --- a/common/app/routes/Jobs/flux/index.js +++ b/common/app/routes/Jobs/flux/index.js @@ -1,2 +1 @@ export { default as JobActions } from './Actions'; -export { default as JobsStore } from './Store'; diff --git a/common/utils/index.js b/common/utils/index.js new file mode 100644 index 0000000000..0456c3bbbe --- /dev/null +++ b/common/utils/index.js @@ -0,0 +1,13 @@ +export function nameSpacedTransformer(ns, transformer) { + if (!transformer) { + return nameSpacedTransformer.bind(null, ns); + } + return (state) => { + const newState = transformer(state[ns]); + // nothing has changed + if (newState === state[ns]) { + return state; + } + return { ...state, [ns]: newState }; + }; +} From e832c2ebc53c29e7c20d953311e49188ce6c9a16 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 10:11:10 -0800 Subject: [PATCH 19/31] Remove debug statement in appActions use arrow func --- common/app/flux/Actions.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index da2318f56d..cc3a3dfe00 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -1,7 +1,5 @@ import { Actions } from 'thundercats'; -import debugFactory from 'debug'; -const debug = debugFactory('freecc:app:actions'); export default Actions({ shouldBindMethods: true, @@ -16,15 +14,14 @@ export default Actions({ return null; } - debug('fetching user data'); return this.readService$('user', null, null) - .map(function({ + .map(({ username, picture, progressTimestamps = [], isFrontEndCert, isFullStackCert - }) { + }) => { return { username, picture, From 6cbcc08c9f65a6999a14d29932823bf055866b47 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:05:01 -0800 Subject: [PATCH 20/31] Fix for name-spaced transformer --- common/utils/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/utils/index.js b/common/utils/index.js index 0456c3bbbe..85f49c563f 100644 --- a/common/utils/index.js +++ b/common/utils/index.js @@ -4,10 +4,13 @@ export function nameSpacedTransformer(ns, transformer) { } return (state) => { const newState = transformer(state[ns]); + // nothing has changed - if (newState === state[ns]) { - return state; + // noop + if (!newState || newState === state[ns]) { + return null; } + return { ...state, [ns]: newState }; }; } From 8e1f8d215948b0b2ffa97f545f588bf7b070d164 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:24:41 -0800 Subject: [PATCH 21/31] Do not refetch if jobs array is not empty --- common/app/routes/Jobs/components/Jobs.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index b648eeb80f..676cd8790a 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -11,6 +11,9 @@ export default contain( return { jobs, showModal }; }, fetchAction: 'jobActions.getJobs', + isPrimed({ jobs = [] }) { + return !!jobs.length; + }, actions: [ 'appActions', 'jobActions' From 53810102dd4c16a1cda0a16cf971922134b1dd18 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:26:14 -0800 Subject: [PATCH 22/31] Use isPrimed api for getUser fetch action --- common/app/App.jsx | 3 +++ common/app/flux/Actions.js | 11 +++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/common/app/App.jsx b/common/app/App.jsx index 1307166924..847542cce4 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -8,6 +8,9 @@ export default contain( { store: 'appStore', fetchAction: 'appActions.getUser', + isPrimed({ username }) { + return !!username; + }, getPayload(props) { return { isPrimed: !!props.username diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index cc3a3dfe00..d0e873b35c 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -1,4 +1,5 @@ import { Actions } from 'thundercats'; +import { Observable } from 'rx'; export default Actions({ @@ -9,11 +10,7 @@ export default Actions({ return { title: title + ' | Free Code Camp' }; }, - getUser({ isPrimed }) { - if (isPrimed) { - return null; - } - + getUser() { return this.readService$('user', null, null) .map(({ username, @@ -30,9 +27,7 @@ export default Actions({ isFullStackCert }; }) - .catch(err => { - console.error(err); - }); + .catch(err => Observable.just({ err })); }, // routing From 4e5da238449f113930ba97e206c5c7ae675923d0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:34:50 -0800 Subject: [PATCH 23/31] Add error handling In the near future these will be handled by a toast. --- client/index.js | 6 ++++++ common/app/routes/Hikes/flux/Actions.js | 17 ++++++----------- common/app/routes/Jobs/flux/Actions.js | 12 +++++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/client/index.js b/client/index.js index e85045329f..c3796092cb 100644 --- a/client/index.js +++ b/client/index.js @@ -53,6 +53,12 @@ app$({ history, location: appLocation }) .doOnNext(title => document.title = title) .subscribe(() => {}); + appStore$ + .pluck('err') + .filter(err => !!err) + .distinctUntilChanged() + .subscribe(err => console.error(err)); + synchroniseHistory( history, updateLocation, diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 4556308871..fb0dd81e8b 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -80,9 +80,9 @@ export default Actions({ } }; }) - .catch(err => { - console.error(err); - }); + .catch(err => Observable.just({ + transform(state) { return { ...state, err }; } + })); }, toggleQuestions() { @@ -243,13 +243,8 @@ export default Actions({ }) .delay(300) .startWith(correctAnswer) - .catch(err => { - console.error(err); - return Observable.just({ - set: { - error: err - } - }); - }); + .catch(err => Observable.just({ + transform(state) { return { ...state, err }; } + })); } }); diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index ee50dd419f..a3717a3653 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,5 +1,7 @@ import { Actions } from 'thundercats'; import store from 'store'; +import { Observable } from 'rx'; + import { nameSpacedTransformer } from '../../../../utils'; const assign = Object.assign; @@ -50,7 +52,7 @@ export default Actions({ }; } })) - .catch(err => ({ + .catch(err => Observable.just({ transform(state) { return { ...state, err }; } @@ -63,7 +65,7 @@ export default Actions({ return { ...state, currentJob: job }; }) })) - .catch(err => ({ + .catch(err => Observable.just({ transform(state) { return { ...state, err }; } @@ -76,9 +78,9 @@ export default Actions({ return { ...state, jobs }; }) })) - .catch(err => ({ + .catch(err => Observable.just({ transform(state) { - return { state, err }; + return { ...state, err }; } })); }, @@ -161,7 +163,7 @@ export default Actions({ })) }; }) - .catch(err => ({ + .catch(err => Observable.just({ transform(state) { return { ...state, err }; } From 81618517c0347f5a02745da94720b87e167b92ea Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:46:54 -0800 Subject: [PATCH 24/31] Fix typo in preview container map --- common/app/routes/Jobs/components/Preview.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 532d0d2cdd..fe8d7868d1 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -13,7 +13,7 @@ export default contain( 'appActions', 'jobActions' ], - map({ jobApp: { form: job = {} } }) { + map({ jobsApp: { form: job = {} } }) { return { job }; } }, From 056bb6a2dd84d5479d51435a43cac7ff2be1fd97 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 17:42:10 -0800 Subject: [PATCH 25/31] Actually use undefined instead of implicit undefined arg Which might not be undefined... --- common/app/routes/Jobs/flux/Actions.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index a3717a3653..b1ac6f5b41 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -45,7 +45,7 @@ export default Actions({ }; return { ...state, - jobs: { + jobsApp: { ...state.jobs, currentJob: job } @@ -169,17 +169,19 @@ export default Actions({ } })); }, - clearPromo(foo, undef) { + clearPromo() { return { + /* eslint-disable no-undefined */ transform: jobsTranformer(state => ({ ...state, - price: undef, - buttonId: undef, - discountAmount: undef, - promoCode: undef, + price: undefined, + buttonId: undefined, + discountAmount: undefined, + promoCode: undefined, promoApplied: false, - promoName: undef + promoName: undefined })) + /* eslint-enable no-undefined */ }; }, init({ instance: jobActions }) { From 5ef6a7f11e9e42e1cde1430f6af5ec6eb11dcf52 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 17:43:01 -0800 Subject: [PATCH 26/31] jobsApp not jobApp --- common/app/routes/Jobs/components/GoToPayPal.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index cf96c9a1c1..a32605e960 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -16,8 +16,8 @@ export default contain( 'jobActions', 'appActions' ], - map({ jobApp: { - job: { id, isHighlighted } = {}, + map({ jobsApp: { + currentJob: { id, isHighlighted } = {}, buttonId = isHighlighted ? paypalIds.highlighted : paypalIds.regular, @@ -25,7 +25,7 @@ export default contain( discountAmount = 0, promoCode = '', promoApplied = false, - promoName + promoName = '' }}) { return { id, From 1fb321332be3969c0da1f74f6f8a593e32cac020 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 21:31:48 -0800 Subject: [PATCH 27/31] Change full stack to back end on job apps --- common/app/flux/Actions.js | 2 ++ common/app/routes/Jobs/components/NewJob.jsx | 28 +++++++++-------- common/app/routes/Jobs/components/Show.jsx | 32 ++++++++++---------- common/models/job.json | 5 +++ 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index d0e873b35c..3594be6c8e 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -17,6 +17,7 @@ export default Actions({ picture, progressTimestamps = [], isFrontEndCert, + isBackEndCert, isFullStackCert }) => { return { @@ -24,6 +25,7 @@ export default Actions({ picture, points: progressTimestamps.length, isFrontEndCert, + isBackEndCert, isFullStackCert }; }) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 1eb9178657..414264f5de 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -115,7 +115,7 @@ export default contain({ logo, company, isFrontEndCert = true, - isFullStackCert, + isBackEndCert, isHighlighted, isRemoteOk, howToApply @@ -132,7 +132,7 @@ export default contain({ isRemoteOk: formatValue(isRemoteOk, null, 'bool'), howToApply: formatValue(howToApply, makeRequired(isAscii)), isFrontEndCert, - isFullStackCert + isBackEndCert }; }, subscribeOnWillMount() { @@ -154,7 +154,7 @@ export default contain({ isHighlighted: PropTypes.object, isRemoteOk: PropTypes.object, isFrontEndCert: PropTypes.bool, - isFullStackCert: PropTypes.bool, + isBackEndCert: PropTypes.bool, howToApply: PropTypes.object }, @@ -171,7 +171,11 @@ export default contain({ } }); - if (!valid || !pros.isFrontEndCert && !pros.isFullStackCert ) { + if ( + !valid || + !pros.isFrontEndCert && + !pros.isBackEndCert + ) { debug('form not valid'); return; } @@ -188,7 +192,7 @@ export default contain({ logo, company, isFrontEndCert, - isFullStackCert, + isBackEndCert, isHighlighted, isRemoteOk, howToApply @@ -207,7 +211,7 @@ export default contain({ isRemoteOk: !!isRemoteOk.value, howToApply: inHTMLData(howToApply.value), isFrontEndCert, - isFullStackCert + isBackEndCert }; const job = Object.keys(jobValues).reduce((accu, prop) => { @@ -237,7 +241,7 @@ export default contain({ handleCertClick(name) { const { jobActions: { handleForm } } = this.props; const otherButton = name === 'isFrontEndCert' ? - 'isFullStackCert' : + 'isBackEndCert' : 'isFrontEndCert'; handleForm({ @@ -259,7 +263,7 @@ export default contain({ isRemoteOk, howToApply, isFrontEndCert, - isFullStackCert, + isBackEndCert, jobActions: { handleForm } } = this.props; @@ -306,13 +310,13 @@ export default contain({