From 8e930fbe8aee450fd115e5f188812371dd3fda13 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 7 Jan 2016 14:51:41 -0800 Subject: [PATCH 01/19] Add toasts to react app --- .eslintrc | 2 +- client/err-saga.js | 9 + ...synchronise-history.js => history-saga.js} | 2 +- client/index.js | 28 +- client/less/main.less | 1 + client/less/toastr.less | 269 ++++++++++++++++++ common/app/App.jsx | 47 ++- common/app/flux/Actions.js | 16 ++ common/app/flux/Store.js | 6 +- common/app/routes/Hikes/flux/Actions.js | 8 +- package.json | 2 + 11 files changed, 373 insertions(+), 17 deletions(-) create mode 100644 client/err-saga.js rename client/{synchronise-history.js => history-saga.js} (96%) create mode 100644 client/less/toastr.less diff --git a/.eslintrc b/.eslintrc index 92fad475c3..0e95beca2c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -227,7 +227,7 @@ "react/jsx-boolean-value": [1, "always"], "jsx-quotes": [1, "prefer-single"], "react/jsx-no-undef": 1, - "react/jsx-sort-props": 1, + "react/jsx-sort-props": [1, { "ignoreCase": true }], "react/jsx-uses-react": 1, "react/jsx-uses-vars": 1, "react/no-did-mount-set-state": 2, diff --git a/client/err-saga.js b/client/err-saga.js new file mode 100644 index 0000000000..d681e4cfcc --- /dev/null +++ b/client/err-saga.js @@ -0,0 +1,9 @@ +export default function toastSaga(err$, toast) { + err$ + .doOnNext(() => toast({ + type: 'error', + title: 'Oops, something went wrong', + message: `Something went wrong, please try again later` + })) + .subscribe(err => console.error(err)); +} diff --git a/client/synchronise-history.js b/client/history-saga.js similarity index 96% rename from client/synchronise-history.js rename to client/history-saga.js index 3e36cf9747..25e65340b9 100644 --- a/client/synchronise-history.js +++ b/client/history-saga.js @@ -20,7 +20,7 @@ const emptyLocation = { let prevKey; let isSyncing = false; -export default function synchroniseHistory( +export default function historySaga( history, updateLocation, goTo, diff --git a/client/index.js b/client/index.js index c3796092cb..ceaeb012ed 100644 --- a/client/index.js +++ b/client/index.js @@ -9,7 +9,8 @@ import { hydrate } from 'thundercats'; import { render$ } from 'thundercats-react'; import { app$ } from '../common/app'; -import synchroniseHistory from './synchronise-history'; +import historySaga from './history-saga'; +import errSaga from './err-saga'; const debug = debugFactory('fcc:client'); const DOMContianer = document.getElementById('fcc'); @@ -38,9 +39,16 @@ app$({ history, location: appLocation }) ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) .doOnNext(({ appCat }) => { - const { updateLocation, goTo, goBack } = appCat.getActions('appActions'); const appStore$ = appCat.getStore('appStore'); + const { + toast, + updateLocation, + goTo, + goBack + } = appCat.getActions('appActions'); + + const routerState$ = appStore$ .map(({ location }) => location) .distinctUntilChanged( @@ -50,22 +58,24 @@ app$({ history, location: appLocation }) // set page title appStore$ .pluck('title') + .distinctUntilChanged() .doOnNext(title => document.title = title) .subscribe(() => {}); - appStore$ - .pluck('err') - .filter(err => !!err) - .distinctUntilChanged() - .subscribe(err => console.error(err)); - - synchroniseHistory( + historySaga( history, updateLocation, goTo, goBack, routerState$ ); + + const err$ = appStore$ + .pluck('err') + .filter(err => !!err) + .distinctUntilChanged(); + + errSaga(err$, toast); }) // allow store subscribe to subscribe to actions .delay(10) diff --git a/client/less/main.less b/client/less/main.less index 69679ccbe2..053e5ddd4c 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -1138,3 +1138,4 @@ code { @import "chat.less"; @import "jobs.less"; @import "challenge.less"; +@import "toastr.less"; diff --git a/client/less/toastr.less b/client/less/toastr.less new file mode 100644 index 0000000000..f4774004a2 --- /dev/null +++ b/client/less/toastr.less @@ -0,0 +1,269 @@ +// sourced from https://github.com/CodeSeven/toastr +// MIT license +// Mix-ins +.borderRadius(@radius) { + -moz-border-radius: @radius; + -webkit-border-radius: @radius; + border-radius: @radius; +} + +.boxShadow(@boxShadow) { + -moz-box-shadow: @boxShadow; + -webkit-box-shadow: @boxShadow; + box-shadow: @boxShadow; +} + +.opacity(@opacity) { + @opacityPercent: @opacity * 100; + opacity: @opacity; + -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})"; + filter: ~"alpha(opacity=@{opacityPercent})"; +} + +.wordWrap(@wordWrap: break-word) { + -ms-word-wrap: @wordWrap; + word-wrap: @wordWrap; +} + +// Variables +@black: #000000; +@grey: #999999; +@light-grey: #CCCCCC; +@white: #FFFFFF; +@near-black: #030303; +@green: #51A351; +@red: #BD362F; +@blue: #2F96B4; +@orange: #F89406; +@default-container-opacity: .8; + +// Styles +.toast-title { + font-weight: bold; +} + +.toast-message { + .wordWrap(); + + a, + label { + color: @white; + } + + a:hover { + color: @light-grey; + text-decoration: none; + } +} + +.toast-close-button { + position: relative; + right: -0.3em; + top: -0.3em; + float: right; + font-size: 20px; + font-weight: bold; + color: @white; + -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1); + text-shadow: 0 1px 0 rgba(255,255,255,1); + .opacity(0.8); + + &:hover, + &:focus { + color: @black; + text-decoration: none; + cursor: pointer; + .opacity(0.4); + } +} + +/*Additional properties for button version + iOS requires the button element instead of an anchor tag. + If you want the anchor version, it requires `href="#"`.*/ +button.toast-close-button { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +//#endregion + +.toast-top-center { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-center { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-full-width { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-full-width { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-left { + top: 12px; + left: 12px; +} + +.toast-top-right { + top: 12px; + right: 12px; +} + +.toast-bottom-right { + right: 12px; + bottom: 12px; +} + +.toast-bottom-left { + bottom: 12px; + left: 12px; +} + +#toast-container { + position: fixed; + z-index: 999999; + // The container should not be clickable. + pointer-events: none; + * { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + + > div { + position: relative; + // The toast itself should be clickable. + pointer-events: auto; + overflow: hidden; + margin: 0 0 6px; + padding: 15px 15px 15px 50px; + width: 300px; + .borderRadius(3px 3px 3px 3px); + background-position: 15px center; + background-repeat: no-repeat; + .boxShadow(0 0 12px @grey); + color: @white; + .opacity(@default-container-opacity); + } + + > :hover { + .boxShadow(0 0 12px @black); + .opacity(1); + cursor: pointer; + } + + > .toast-info { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; + } + + > .toast-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; + } + + > .toast-success { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; + } + + > .toast-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; + } + + /*overrides*/ + &.toast-top-center > div, + &.toast-bottom-center > div { + width: 300px; + margin-left: auto; + margin-right: auto; + } + + &.toast-top-full-width > div, + &.toast-bottom-full-width > div { + width: 96%; + margin-left: auto; + margin-right: auto; + } +} + +.toast { + background-color: @near-black; +} + +.toast-success { + background-color: @green; +} + +.toast-error { + background-color: @red; +} + +.toast-info { + background-color: @blue; +} + +.toast-warning { + background-color: @orange; +} + +.toast-progress { + position: absolute; + left: 0; + bottom: 0; + height: 4px; + background-color: @black; + .opacity(0.4); +} + +/*Responsive Design*/ + +@media all and (max-width: 240px) { + #toast-container { + + > div { + padding: 8px 8px 8px 50px; + width: 11em; + } + + & .toast-close-button { + right: -0.2em; + top: -0.2em; + } + } +} + +@media all and (min-width: 241px) and (max-width: 480px) { + #toast-container { + > div { + padding: 8px 8px 8px 50px; + width: 18em; + } + + & .toast-close-button { + right: -0.2em; + top: -0.2em; + } + } +} + +@media all and (min-width: 481px) and (max-width: 768px) { + #toast-container { + > div { + padding: 15px 15px 15px 50px; + width: 25em; + } + } +} diff --git a/common/app/App.jsx b/common/app/App.jsx index 847542cce4..69e7d8b082 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -1,16 +1,33 @@ import React, { PropTypes } from 'react'; -import { contain } from 'thundercats-react'; import { Row } from 'react-bootstrap'; +import { ToastMessage, ToastContainer } from 'react-toastr'; +import { contain } from 'thundercats-react'; import { Nav } from './components/Nav'; +const toastMessageFactory = React.createFactory(ToastMessage.animation); + export default contain( { + actions: ['appActions'], store: 'appStore', fetchAction: 'appActions.getUser', isPrimed({ username }) { return !!username; }, + map({ + username, + points, + picture, + toast + }) { + return { + username, + points, + picture, + toast + }; + }, getPayload(props) { return { isPrimed: !!props.username @@ -21,11 +38,31 @@ export default contain( displayName: 'FreeCodeCamp', propTypes: { + appActions: PropTypes.object, children: PropTypes.node, + username: PropTypes.string, points: PropTypes.number, picture: PropTypes.string, - title: PropTypes.string, - username: PropTypes.string + toast: PropTypes.object + }, + + componentWillReceiveProps({ toast: nextToast }) { + const { toast = {} } = this.props; + if ( + toast && + nextToast && + toast.id !== nextToast.id + ) { + + this.refs.toaster[nextToast.type || 'success']( + nextToast.message, + nextToast.title, + { + closeButton: true, + timeOut: 10000 + } + ); + } }, render() { @@ -38,6 +75,10 @@ export default contain( { this.props.children } + ); } diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 3594be6c8e..3a0915be93 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -33,8 +33,24 @@ export default Actions({ }, // routing + // goTo(path: String) => path goTo: null, + + // goBack(arg?) => arg? goBack: null, + + // toast(args: { type?: String, message: String, title: String }) => args + toast(args) { + return { + transform(state) { + const id = state.toast && state.toast.id ? state.toast.id : 0; + const toast = { ...args, id: id + 1 }; + return { ...state, toast }; + } + }; + }, + + // updateLocation(location: { pathname: String }) => location updateLocation(location) { return { transform(state) { diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index 36116ad7b0..2914bc268c 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -28,7 +28,8 @@ export default Store({ const { updateLocation, getUser, - setTitle + setTitle, + toast } = cat.getActions('appActions'); register( @@ -39,7 +40,8 @@ export default Store({ setTitle ) ), - updateLocation + updateLocation, + toast ) ); diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 5bf63fe56d..580ff0e074 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -242,7 +242,13 @@ export default Actions({ return { ...state, points: username ? state.points + 1 : state.points, - hikesApp + hikesApp, + toast: { + title: 'Congratulations!', + message: 'Hike completed', + id: state.toast && state.toast.id ? state.toast.id + 1 : 0, + type: 'success' + } }; }, optimistic: optimisticSave diff --git a/package.json b/package.json index f4de269e23..89925a45c3 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "express-session": "^1.12.1", "express-state": "^1.2.0", "express-validator": "^2.18.0", + "fbjs": "^0.6.0", "fetchr": "~0.5.12", "forever": "~0.15.1", "frameguard": "~0.2.2", @@ -109,6 +110,7 @@ "react-motion": "~0.3.1", "react-router": "^1.0.0", "react-router-bootstrap": "https://github.com/FreeCodeCamp/react-router-bootstrap.git#freecodecamp", + "react-toastr": "^2.3.0", "react-vimeo": "~0.0.3", "request": "^2.65.0", "rev-del": "^1.0.5", From 9a9421e712e830c27ae96710d5025c3c40fe5831 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 7 Jan 2016 22:40:51 -0800 Subject: [PATCH 02/19] Fix for Vimeo API change --- package.json | 2 +- server/middlewares/csp.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 89925a45c3..b83391cf75 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "react-router": "^1.0.0", "react-router-bootstrap": "https://github.com/FreeCodeCamp/react-router-bootstrap.git#freecodecamp", "react-toastr": "^2.3.0", - "react-vimeo": "~0.0.3", + "react-vimeo": "~0.1.0", "request": "^2.65.0", "rev-del": "^1.0.5", "rx": "^4.0.0", diff --git a/server/middlewares/csp.js b/server/middlewares/csp.js index 21e542dd01..2aaac24d18 100644 --- a/server/middlewares/csp.js +++ b/server/middlewares/csp.js @@ -24,7 +24,8 @@ export default function csp() { 'https://*.jsdelivr.com', '*.jsdelivr.com', '*.twimg.com', - 'https://*.twimg.com' + 'https://*.twimg.com', + 'vimeo.com' ].concat(trusted), connectSrc: [ 'vimeo.com' From 268bbe52e65b40f1a02a8c386b7739cd9c454d8b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 8 Jan 2016 10:24:30 -0800 Subject: [PATCH 03/19] Left align and enlarge text in lecture --- common/app/routes/Hikes/components/Lecture.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx index 3f18b97b89..77d7fe5c9c 100644 --- a/common/app/routes/Hikes/components/Lecture.jsx +++ b/common/app/routes/Hikes/components/Lecture.jsx @@ -52,7 +52,11 @@ export default contain( renderTranscript(transcript, dashedName) { return transcript.map((line, index) => ( -

{ line }

+

+ { line } +

)); }, From 5c1453bbaadadfba417ab59f2265daa8ee2ba0b5 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 8 Jan 2016 21:43:53 -0800 Subject: [PATCH 04/19] Fix toast not showing up multiple times. --- common/app/routes/Hikes/flux/Actions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 580ff0e074..5220286759 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -246,7 +246,9 @@ export default Actions({ toast: { title: 'Congratulations!', message: 'Hike completed', - id: state.toast && state.toast.id ? state.toast.id + 1 : 0, + id: state.toast && typeof state.toast.id === 'number' ? + state.toast.id + 1 : + 0, type: 'success' } }; From d9abe3dddc527f104dd7b1aec66239d4e168ce19 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 17:00:49 -0800 Subject: [PATCH 05/19] Use release to indicate answer attempt --- .../app/routes/Hikes/components/Questions.jsx | 99 +++++----- common/app/routes/Hikes/flux/Actions.js | 170 +++++++++++------- 2 files changed, 150 insertions(+), 119 deletions(-) diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index 1a7b08c9e3..38dade4633 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -9,7 +9,7 @@ import { Row } from 'react-bootstrap'; -const ANSWER_THRESHOLD = 200; +const answerThreshold = 200; export default contain( { @@ -35,7 +35,7 @@ export default contain( isPressed, showInfo, shake, - username + isSignedIn: !!username }; } }, @@ -51,56 +51,46 @@ export default contain( isPressed: PropTypes.bool, showInfo: PropTypes.bool, shake: PropTypes.bool, - username: PropTypes.string, + isSignedIn: PropTypes.bool, hikesActions: PropTypes.object }, - handleMouseDown({ pageX, pageY, touches }) { - if (touches) { - ({ pageX, pageY } = touches[0]); - } - const { mouse: [pressX, pressY], hikesActions } = this.props; - hikesActions.grabQuestion({ pressX, pressY, pageX, pageY }); - }, - - handleMouseUp() { + handleMouseUp(e, answer) { + e.stopPropagation(); if (!this.props.isPressed) { return null; } + + const { + hike, + currentQuestion, + isSignedIn, + delta + } = this.props; + this.props.hikesActions.releaseQuestion(); + this.props.hikesActions.answer({ + e, + answer, + hike, + delta, + currentQuestion, + isSignedIn, + threshold: answerThreshold + }); }, - handleMouseMove(answer) { + handleMouseMove(e) { if (!this.props.isPressed) { - return () => {}; + return null; } + const { delta, hikesActions } = this.props; - return (e) => { - let { pageX, pageY, touches } = e; - - if (touches) { - e.preventDefault(); - // these re-assigns the values of pageX, pageY from touches - ({ pageX, pageY } = touches[0]); - } - - 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); - }; + hikesActions.moveQuestion({ e, delta }); }, onAnswer(answer, userAnswer) { - const { hikesActions } = this.props; + const { isSignedIn, hike, hikesActions } = this.props; return (e) => { if (e && e.preventDefault) { e.preventDefault(); @@ -109,20 +99,12 @@ export default contain( return hikesActions.answer({ answer, userAnswer, - props: this.props + hike, + isSignedIn }); }; }, - routerWillLeave(nextState, router, cb) { - // TODO(berks): do animated transitions here stuff here - this.setState({ - showInfo: false, - isCorrect: false, - mouse: [0, 0] - }, cb); - }, - renderInfo(showInfo, info, hideInfo) { if (!info) { return null; @@ -150,6 +132,8 @@ export default contain( }, renderQuestion(number, question, answer, shake) { + const { hikesActions } = this.props; + const mouseUp = e => this.handleMouseUp(e, answer); return ({ x }) => { const style = { WebkitTransform: `translate3d(${ x }px, 0, 0)`, @@ -160,13 +144,13 @@ export default contain(

{ question }

@@ -175,19 +159,20 @@ export default contain( }, render() { - const { showInfo, shake } = this.props; const { hike: { tests = [] } = {}, mouse: [x], currentQuestion, - hikesActions + hikesActions, + showInfo, + shake } = this.props; const [ question, answer, info ] = tests[currentQuestion - 1] || []; return ( this.handleMouseUp(e, answer) } xs={ 8 } xsOffset={ 2 }> diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 5220286759..25c879d302 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -4,6 +4,7 @@ import { Actions } from 'thundercats'; import debugFactory from 'debug'; const debug = debugFactory('freecc:hikes:actions'); +const noOp = { transform: () => {} }; function getCurrentHike(hikes = [{}], dashedName, currentHike) { if (!dashedName) { @@ -35,18 +36,17 @@ 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 }; +function getMouse(e, [dx, dy]) { + let { pageX, pageY, touches } = e; + + if (touches) { + e.preventDefault(); + // these re-assigns the values of pageX, pageY from touches + ({ pageX, pageY } = touches[0]); + } + + return [pageX - dx, pageY - dy]; } export default Actions({ @@ -107,64 +107,115 @@ export default Actions({ }; }, - grabQuestion({ pressX, pressY, pageX, pageY }) { - const dx = pageX - pressX; - const dy = pageY - pressY; - - const delta = [dx, dy]; - const mouse = [pageX - dx, pageY - dy]; + grabQuestion(e) { + const { pageX, pageY } = e; + const delta = [pageX, pageY]; + const mouse = getMouse(e, delta); return { transform(state) { - const hikesApp = { ...state.hikesApp, isPressed: true, delta, mouse }; - return { ...state, hikesApp }; + return { + ...state, + hikesApp: { + ...state.hikesApp, + isPressed: true, + delta, + mouse + } + }; } }; }, releaseQuestion() { - return { transform: releaseQuestion }; - }, - - moveQuestion(mouse) { return { transform(state) { - const hikesApp = { ...state.hikesApp, mouse }; - return { ...state, hikesApp }; + return { + ...state, + hikesApp: { + ...state.hikesApp, + isPressed: false, + mouse: [0, 0] + } + }; + } + }; + }, + + moveQuestion({ e, delta }) { + const mouse = getMouse(e, delta); + + return { + transform(state) { + return { + ...state, + hikesApp: { + ...state.hikesApp, + mouse + } + }; } }; }, answer({ + e, answer, userAnswer, - props: { - hike: { id, name, tests, challengeType }, - currentQuestion, - username - } + hike: { id, name, tests, challengeType }, + currentQuestion, + isSignedIn, + delta, + threshold }) { + if (typeof userAnswer === 'undefined') { + const [positionX] = getMouse(e, delta); + + // question released under threshold + if (Math.abs(positionX) < threshold) { + return noOp; + } + + if (positionX >= threshold) { + userAnswer = true; + } + + if (positionX <= -threshold) { + userAnswer = false; + } + } // incorrect question if (answer !== userAnswer) { const startShake = { transform(state) { - const hikesApp = { ...state.hikesApp, showInfo: true, shake: true }; - return { ...state, hikesApp }; + return { + ...state, + hikesApp: { + ...state.hikesApp, + showInfo: true, + shake: true + } + }; } }; const removeShake = { transform(state) { - const hikesApp = { ...state.hikesApp, shake: false }; - return { ...state, hikesApp }; + return { + ...state, + hikesApp: { + ...state.hikesApp, + shake: false + } + }; } }; return Observable .just(removeShake) .delay(500) - .startWith({ transform: releaseQuestion }, startShake); + .startWith(startShake); } // move to next question @@ -198,22 +249,21 @@ export default Actions({ } // challenge completed - const optimisticSave = username ? + const optimisticSave = isSignedIn ? this.post$('/completed-challenge', { id, name, challengeType }) : Observable.just(true); const correctAnswer = { transform(state) { - const hikesApp = { - ...state.hikesApp, - isCorrect: true, - isPressed: false, - delta: [0, 0], - mouse: [ userAnswer ? 1000 : -1000, 0] - }; return { ...state, - hikesApp + hikesApp: { + ...state.hikesApp, + isCorrect: true, + isPressed: false, + delta: [0, 0], + mouse: [ userAnswer ? 1000 : -1000, 0] + } }; } }; @@ -223,26 +273,16 @@ export default Actions({ const { hikes, currentHike: { id } } = state.hikesApp; const currentHike = findNextHike(hikes, id); - // go to next route - state.location = { - action: 'PUSH', - pathname: currentHike && currentHike.dashedName ? - `/hikes/${ currentHike.dashedName }` : - '/hikes' - }; - - const hikesApp = { - ...state.hikesApp, - currentHike, - showQuestions: false, - currentQuestion: 1, - mouse: [0, 0] - }; - return { ...state, - points: username ? state.points + 1 : state.points, - hikesApp, + points: isSignedIn ? state.points + 1 : state.points, + hikesApp: { + ...state.hikesApp, + currentHike, + showQuestions: false, + currentQuestion: 1, + mouse: [0, 0] + }, toast: { title: 'Congratulations!', message: 'Hike completed', @@ -250,6 +290,12 @@ export default Actions({ state.toast.id + 1 : 0, type: 'success' + }, + location: { + action: 'PUSH', + pathname: currentHike && currentHike.dashedName ? + `/hikes/${ currentHike.dashedName }` : + '/hikes' } }; }, From 019a28a5cc764f117488e3683527cb91630e506f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 18:16:10 -0800 Subject: [PATCH 06/19] Fix remove semi-colon guard Users should be instructed to always use semi-colons --- client/commonFramework/end.js | 2 +- client/commonFramework/execute-challenge-stream.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/commonFramework/end.js b/client/commonFramework/end.js index b28a29e9fb..3948f0aeb2 100644 --- a/client/commonFramework/end.js +++ b/client/commonFramework/end.js @@ -35,7 +35,7 @@ $(document).ready(function() { .flatMap(code => { return common.detectUnsafeCode$(code) .map(() => { - const combinedCode = common.head + '\n;;' + code + '\n;;' + common.tail; + const combinedCode = common.head + code + common.tail; return addLoopProtect(combinedCode); }) diff --git a/client/commonFramework/execute-challenge-stream.js b/client/commonFramework/execute-challenge-stream.js index 29af8f7279..b81de64486 100644 --- a/client/commonFramework/execute-challenge-stream.js +++ b/client/commonFramework/execute-challenge-stream.js @@ -18,7 +18,7 @@ window.common = (function(global) { const originalCode = code; const head = common.arrayToNewLineString(common.head); const tail = common.arrayToNewLineString(common.tail); - const combinedCode = head + '\n;;' + code + '\n;;' + tail; + const combinedCode = head + code + tail; ga('send', 'event', 'Challenge', 'ran-code', common.challengeName); From 9acc946a4e191170c4a5ce598b2b81d1c83ded01 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 18:38:15 -0800 Subject: [PATCH 07/19] Fix for no key issue Now if location has no key, it is assumed that history hasn't updated --- client/history-saga.js | 2 +- client/index.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/client/history-saga.js b/client/history-saga.js index 25e65340b9..f73576d394 100644 --- a/client/history-saga.js +++ b/client/history-saga.js @@ -35,7 +35,7 @@ export default function historySaga( } // store location has changed, update history - if (location.key !== prevKey) { + if (!location.key || location.key !== prevKey) { isSyncing = true; history.transitionTo({ ...emptyLocation, ...location }); isSyncing = false; diff --git a/client/index.js b/client/index.js index ceaeb012ed..5089d27322 100644 --- a/client/index.js +++ b/client/index.js @@ -51,9 +51,7 @@ app$({ history, location: appLocation }) const routerState$ = appStore$ .map(({ location }) => location) - .distinctUntilChanged( - location => location && location.key ? location.key : location - ); + .filter(location => !!location); // set page title appStore$ From 850c63a0a09c32a033cf27c7324656e6904ca68e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 18:40:07 -0800 Subject: [PATCH 08/19] Fix on button answer must past in current Question --- common/app/routes/Hikes/components/Questions.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index 38dade4633..ae91b2cfa8 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -90,7 +90,7 @@ export default contain( }, onAnswer(answer, userAnswer) { - const { isSignedIn, hike, hikesActions } = this.props; + const { isSignedIn, hike, currentQuestion, hikesActions } = this.props; return (e) => { if (e && e.preventDefault) { e.preventDefault(); @@ -99,6 +99,7 @@ export default contain( return hikesActions.answer({ answer, userAnswer, + currentQuestion, hike, isSignedIn }); From 7e2867f7477b313cf50cff6a33f8b66f15b5af4e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 19:59:17 -0800 Subject: [PATCH 09/19] Fix ajax json requests should be application/json --- common/utils/ajax-stream.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/common/utils/ajax-stream.js b/common/utils/ajax-stream.js index ba7b428b2a..0e02ba8ad3 100644 --- a/common/utils/ajax-stream.js +++ b/common/utils/ajax-stream.js @@ -272,7 +272,10 @@ export function postJSON$(url, body) { body, method: 'POST', responseType: 'json', - headers: { 'Content-Type': 'application/json' } + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } }); } @@ -294,7 +297,14 @@ export function get$(url) { * @returns {Observable} The observable sequence which contains the parsed JSON */ export function getJSON$(url) { - return ajax$({ url: url, responseType: 'json' }).map(function(x) { + return ajax$({ + url: url, + responseType: 'json', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }).map(function(x) { return x.response; }); } From e51127ffc7ea899e78174082636bbca5938ad475 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 20:08:01 -0800 Subject: [PATCH 10/19] feature completed challenge can also return json --- server/boot/challenge.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/boot/challenge.js b/server/boot/challenge.js index 63234e4bd4..978c8a8e7a 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -4,6 +4,7 @@ import moment from 'moment'; import { Observable, Scheduler } from 'rx'; import assign from 'object.assign'; import debugFactory from 'debug'; +import accepts from 'accepts'; import { dasherize, @@ -81,7 +82,8 @@ function updateUserProgress(user, challengeId, completedChallenge) { lastUpdated: completedChallenge.completedDate } ); - return user; + + return { user, alreadyCompleted }; } @@ -373,6 +375,7 @@ module.exports = function(app) { } function completedChallenge(req, res, next) { + const type = accepts(req).type('html', 'json', 'text'); const completedDate = Math.round(+new Date()); const { @@ -382,7 +385,7 @@ module.exports = function(app) { solution } = req.body; - updateUserProgress( + const { alreadyCompleted } = updateUserProgress( req.user, id, { @@ -395,9 +398,11 @@ module.exports = function(app) { } ); + let user = req.user; saveUser(req.user) .subscribe( function(user) { + user = user; debug( 'user save points %s', user && user.progressTimestamps && user.progressTimestamps.length @@ -405,6 +410,12 @@ module.exports = function(app) { }, next, function() { + if (type === 'json') { + return res.json({ + points: user.progressTimestamps.length, + alreadyCompleted + }); + } res.sendStatus(200); } ); From 835a47828229db7dd5d8d78642466389bcd8e546 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:18:17 -0800 Subject: [PATCH 11/19] Fix grabQuestion on mobile --- common/app/routes/Hikes/flux/Actions.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index 25c879d302..f93f5932fb 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -108,9 +108,14 @@ export default Actions({ }, grabQuestion(e) { - const { pageX, pageY } = e; + let { pageX, pageY, touches } = e; + if (touches) { + e.preventDefault(); + // these re-assigns the values of pageX, pageY from touches + ({ pageX, pageY } = touches[0]); + } const delta = [pageX, pageY]; - const mouse = getMouse(e, delta); + const mouse = [0, 0]; return { transform(state) { From 632f596821e2dd1e7946eb3359fd97f5f3906823 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:47:28 -0800 Subject: [PATCH 12/19] Fix mobile touches can be empty. Use changedTouches as backup --- common/app/routes/Hikes/flux/Actions.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index f93f5932fb..dcde5c2595 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -38,12 +38,13 @@ function findNextHike(hikes, id) { function getMouse(e, [dx, dy]) { - let { pageX, pageY, touches } = e; + let { pageX, pageY, touches, changedTouches } = e; - if (touches) { + // touches can be empty on touchend + if (touches || changedTouches) { e.preventDefault(); // these re-assigns the values of pageX, pageY from touches - ({ pageX, pageY } = touches[0]); + ({ pageX, pageY } = touches[0] || changedTouches[0]); } return [pageX - dx, pageY - dy]; From f65b6472ddedf2db2df9241d7fe7b3cc67bcf0a2 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:48:48 -0800 Subject: [PATCH 13/19] Clean up toast logic in App.jsx --- common/app/App.jsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/common/app/App.jsx b/common/app/App.jsx index 69e7d8b082..6099540c7d 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -46,14 +46,9 @@ export default contain( toast: PropTypes.object }, - componentWillReceiveProps({ toast: nextToast }) { + componentWillReceiveProps({ toast: nextToast = {} }) { const { toast = {} } = this.props; - if ( - toast && - nextToast && - toast.id !== nextToast.id - ) { - + if (toast.id !== nextToast.id) { this.refs.toaster[nextToast.type || 'success']( nextToast.message, nextToast.title, From a8ee090fead6e4ef9d8c198880941048e11811be Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:49:16 -0800 Subject: [PATCH 14/19] Clean up toast logic in AppActions --- common/app/flux/Actions.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 3a0915be93..8fb5025d0a 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -43,9 +43,13 @@ export default Actions({ toast(args) { return { transform(state) { - const id = state.toast && state.toast.id ? state.toast.id : 0; - const toast = { ...args, id: id + 1 }; - return { ...state, toast }; + return { + ...state, + toast: { + ...args, + id: state.toast && state.toast.id ? state.toast.id : 1 + } + }; } }; }, From c670cdfaab553f4e84f16175410c560ac22e6c01 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:05:53 -0800 Subject: [PATCH 15/19] Fix postJSON$ should return response --- common/utils/ajax-stream.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/utils/ajax-stream.js b/common/utils/ajax-stream.js index 0e02ba8ad3..665e23348c 100644 --- a/common/utils/ajax-stream.js +++ b/common/utils/ajax-stream.js @@ -276,7 +276,8 @@ export function postJSON$(url, body) { 'Content-Type': 'application/json', 'Accept': 'application/json' } - }); + }) + .map(({ response }) => response); } /** @@ -304,7 +305,5 @@ export function getJSON$(url) { 'Content-Type': 'application/json', 'Accept': 'application/json' } - }).map(function(x) { - return x.response; - }); + }).map(({ response }) => response); } From e3890785a2d328b246f4859f3d8608bdfefd621e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:21:03 -0800 Subject: [PATCH 16/19] Reduce threshold for mobile --- common/app/routes/Hikes/components/Questions.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index ae91b2cfa8..bedb202a57 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -9,7 +9,7 @@ import { Row } from 'react-bootstrap'; -const answerThreshold = 200; +const answerThreshold = 100; export default contain( { From d172edecf77610f6e426d128d11b659260dd62e5 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:21:19 -0800 Subject: [PATCH 17/19] Remove optimistic update Add multiple toast when saving challenge and when first completed challenges --- common/app/routes/Hikes/flux/Actions.js | 102 +++++++++++++++--------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/common/app/routes/Hikes/flux/Actions.js b/common/app/routes/Hikes/flux/Actions.js index dcde5c2595..955afb566c 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -255,9 +255,71 @@ export default Actions({ } // challenge completed - const optimisticSave = isSignedIn ? - this.post$('/completed-challenge', { id, name, challengeType }) : - Observable.just(true); + let update$; + if (isSignedIn) { + const body = { id, name, challengeType }; + update$ = this.postJSON$('/completed-challenge', body) + // if post fails, will retry once + .retry(3) + .map(({ alreadyCompleted, points }) => ({ + transform(state) { + return { + ...state, + points, + toast: { + message: + 'Challenge saved.' + + (alreadyCompleted ? '' : ' First time Completed!'), + title: 'Saved', + type: 'info', + id: state.toast && state.toast.id ? state.toast.id + 1 : 1 + } + }; + } + })) + .catch((errObj => { + const err = new Error(errObj.message); + err.stack = errObj.stack; + return { + transform(state) { return { ...state, err }; } + }; + })); + } else { + update$ = Observable.just({ transform: (() => {}) }); + } + + const challengeCompleted$ = Observable.just({ + transform(state) { + const { hikes, currentHike: { id } } = state.hikesApp; + const currentHike = findNextHike(hikes, id); + + return { + ...state, + points: isSignedIn ? state.points + 1 : state.points, + hikesApp: { + ...state.hikesApp, + currentHike, + showQuestions: false, + currentQuestion: 1, + mouse: [0, 0] + }, + toast: { + title: 'Congratulations!', + message: 'Hike completed.' + (isSignedIn ? ' Saving...' : ''), + id: state.toast && state.toast.id ? + state.toast.id + 1 : + 1, + type: 'success' + }, + location: { + action: 'PUSH', + pathname: currentHike && currentHike.dashedName ? + `/hikes/${ currentHike.dashedName }` : + '/hikes' + } + }; + } + }); const correctAnswer = { transform(state) { @@ -274,39 +336,7 @@ export default Actions({ } }; - return Observable.just({ - transform(state) { - const { hikes, currentHike: { id } } = state.hikesApp; - const currentHike = findNextHike(hikes, id); - - return { - ...state, - points: isSignedIn ? state.points + 1 : state.points, - hikesApp: { - ...state.hikesApp, - currentHike, - showQuestions: false, - currentQuestion: 1, - mouse: [0, 0] - }, - toast: { - title: 'Congratulations!', - message: 'Hike completed', - id: state.toast && typeof state.toast.id === 'number' ? - state.toast.id + 1 : - 0, - type: 'success' - }, - location: { - action: 'PUSH', - pathname: currentHike && currentHike.dashedName ? - `/hikes/${ currentHike.dashedName }` : - '/hikes' - } - }; - }, - optimistic: optimisticSave - }) + return Observable.merge(challengeCompleted$, update$) .delay(300) .startWith(correctAnswer) .catch(err => Observable.just({ From 2962ee52a0cb1731793e71edb53868e035ab6051 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:23:04 -0800 Subject: [PATCH 18/19] Remove old debug --- server/boot/challenge.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/boot/challenge.js b/server/boot/challenge.js index 978c8a8e7a..c88e377b04 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -403,10 +403,6 @@ module.exports = function(app) { .subscribe( function(user) { user = user; - debug( - 'user save points %s', - user && user.progressTimestamps && user.progressTimestamps.length - ); }, next, function() { From 8ba64d4950277839c8978cf94158b98ee14e022f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:40:42 -0800 Subject: [PATCH 19/19] Feature use toast instead of modal for info --- common/app/flux/Store.js | 2 - .../app/routes/Hikes/components/Questions.jsx | 65 +++++-------------- common/app/routes/Hikes/flux/Actions.js | 25 ++++--- 3 files changed, 30 insertions(+), 62 deletions(-) diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index 2914bc268c..49f7cb342d 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -49,7 +49,6 @@ export default Store({ const { toggleQuestions, fetchHikes, - hideInfo, resetHike, grabQuestion, releaseQuestion, @@ -61,7 +60,6 @@ export default Store({ fromMany( toggleQuestions, fetchHikes, - hideInfo, resetHike, grabQuestion, releaseQuestion, diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/Hikes/components/Questions.jsx index bedb202a57..0ac01fb9ee 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/Hikes/components/Questions.jsx @@ -1,13 +1,7 @@ import React, { PropTypes } from 'react'; import { spring, Motion } from 'react-motion'; import { contain } from 'thundercats-react'; -import { - Button, - Col, - Modal, - Panel, - Row -} from 'react-bootstrap'; +import { Button, Col, Panel, Row } from 'react-bootstrap'; const answerThreshold = 100; @@ -23,7 +17,6 @@ export default contain( isCorrect = false, delta = [0, 0], isPressed = false, - showInfo = false, shake = false } = hikesApp; return { @@ -33,7 +26,6 @@ export default contain( isCorrect, delta, isPressed, - showInfo, shake, isSignedIn: !!username }; @@ -49,13 +41,12 @@ export default contain( isCorrect: PropTypes.bool, delta: PropTypes.array, isPressed: PropTypes.bool, - showInfo: PropTypes.bool, shake: PropTypes.bool, isSignedIn: PropTypes.bool, hikesActions: PropTypes.object }, - handleMouseUp(e, answer) { + handleMouseUp(e, answer, info) { e.stopPropagation(); if (!this.props.isPressed) { return null; @@ -76,6 +67,7 @@ export default contain( delta, currentQuestion, isSignedIn, + info, threshold: answerThreshold }); }, @@ -89,7 +81,7 @@ export default contain( hikesActions.moveQuestion({ e, delta }); }, - onAnswer(answer, userAnswer) { + onAnswer(answer, userAnswer, info) { const { isSignedIn, hike, currentQuestion, hikesActions } = this.props; return (e) => { if (e && e.preventDefault) { @@ -101,40 +93,15 @@ export default contain( userAnswer, currentQuestion, hike, + info, isSignedIn }); }; }, - renderInfo(showInfo, info, hideInfo) { - if (!info) { - return null; - } - return ( - - -

- { info } -

-
- - - -
- ); - }, - - renderQuestion(number, question, answer, shake) { + renderQuestion(number, question, answer, shake, info) { const { hikesActions } = this.props; - const mouseUp = e => this.handleMouseUp(e, answer); + const mouseUp = e => this.handleMouseUp(e, answer, info); return ({ x }) => { const style = { WebkitTransform: `translate3d(${ x }px, 0, 0)`, @@ -164,34 +131,38 @@ export default contain( hike: { tests = [] } = {}, mouse: [x], currentQuestion, - hikesActions, - showInfo, shake } = this.props; const [ question, answer, info ] = tests[currentQuestion - 1] || []; + const questionElement = this.renderQuestion( + currentQuestion, + question, + answer, + shake, + info + ); return ( this.handleMouseUp(e, answer) } + onMouseUp={ e => this.handleMouseUp(e, answer, info) } xs={ 8 } xsOffset={ 2 }> - { this.renderQuestion(currentQuestion, question, answer, shake) } + { questionElement } - { 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 955afb566c..2ca539c731 100644 --- a/common/app/routes/Hikes/flux/Actions.js +++ b/common/app/routes/Hikes/flux/Actions.js @@ -99,15 +99,6 @@ export default Actions({ }; }, - hideInfo() { - return { - transform(state) { - const hikesApp = { ...state.hikesApp, showInfo: false }; - return { ...state, hikesApp }; - } - }; - }, - grabQuestion(e) { let { pageX, pageY, touches } = e; if (touches) { @@ -172,6 +163,7 @@ export default Actions({ currentQuestion, isSignedIn, delta, + info, threshold }) { if (typeof userAnswer === 'undefined') { @@ -195,11 +187,20 @@ export default Actions({ if (answer !== userAnswer) { const startShake = { transform(state) { + const toast = !info ? + state.toast : + { + id: state.toast && state.toast.id ? state.toast.id + 1 : 1, + title: 'Hint', + message: info, + type: 'info' + }; + return { ...state, + toast, hikesApp: { ...state.hikesApp, - showInfo: true, shake: true } }; @@ -232,8 +233,7 @@ export default Actions({ transform(state) { const hikesApp = { ...state.hikesApp, - mouse: [0, 0], - showInfo: false + mouse: [0, 0] }; return { ...state, hikesApp }; } @@ -351,7 +351,6 @@ export default Actions({ ...state.hikesApp, currentQuestion: 1, showQuestions: false, - showInfo: false, mouse: [0, 0], delta: [0, 0] }