From fa4b65e134f0d7fbf42da572331d63ed8d498a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20G=C4=99bicki?= Date: Sat, 9 Jan 2016 17:36:10 +0100 Subject: [PATCH 01/75] Correct mistyping in 'separated' word --- .../basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index d1ca00ef8f..ab3f4aa0ef 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -3686,7 +3686,7 @@ "description": [ "You can run the same code multiple times by using a loop.", "The most common type of JavaScript loop is called a \"for loop\" because it runs \"for\" a specific number of times.", - "For loops are declared with three optional expressions seperated by semicolons:", + "For loops are declared with three optional expressions separated by semicolons:", "for ([initialization]; [condition]; [final-expression])", "The initialization statement is executed one time only before the loop starts. It is typically used to define and setup your loop variable.", "The condition statement is evaluated at the beginning of every loop iteration and will continue as long as it evalutes to true. When condition is false at the start of the iteration, the loop will stop executing. This means if condition starts as false, your loop will never execute.", From 8f777f62d833c3404c3a3acf4d234bd8a454b31a Mon Sep 17 00:00:00 2001 From: patsul12 Date: Fri, 1 Jan 2016 14:31:07 -0800 Subject: [PATCH 02/75] add a check for single line, multi-line formatted comments, to prevent loop protect from triggering in these instances --- public/js/lib/loop-protect/loop-protect.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/js/lib/loop-protect/loop-protect.js b/public/js/lib/loop-protect/loop-protect.js index 341fb6fa5f..ac3376b6e1 100644 --- a/public/js/lib/loop-protect/loop-protect.js +++ b/public/js/lib/loop-protect/loop-protect.js @@ -50,7 +50,6 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } var openPos = -1; do { - j -= 1; DEBUG && debug('looking backwards ' + lines[j]); // jshint ignore:line closePos = lines[j].indexOf('*/'); openPos = lines[j].indexOf('/*'); @@ -59,6 +58,11 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } closeCommentTags++; } + // check for single line /* comment */ formatted comments + if (closePos === lines[j].length - 2 && openPos !== -1) { + closeCommentTags--; + } + if (openPos !== -1) { closeCommentTags--; @@ -67,6 +71,7 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } return true; } } + j -= 1; } while (j !== 0); return false; From bf05f8804542afa63072ae079b0823bf4a3af29f Mon Sep 17 00:00:00 2001 From: Harsha Date: Sun, 3 Jan 2016 01:05:10 -0500 Subject: [PATCH 03/75] add tail to escape-sequences waypoint --- .../basic-javascript.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index ab3f4aa0ef..e0f3cfd7ec 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -871,6 +871,12 @@ "", "" ], + "tail": [ + "(function(){", + "if (myStr !== undefined){", + "return 'myStr = '+ JSON.stringify(myStr);}", + "else{return null;}})();" + ], "solutions": [ "var myStr = \"\\\\ \\t \\t \\r \\n\";" ], From 9ba5d2c44826df6ac6b3933b4ae299c765bf7609 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 20 Dec 2015 19:24:38 -0800 Subject: [PATCH 04/75] 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 f56a20376c4038142291ca2c9f231054a6823375 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:21:39 -0800 Subject: [PATCH 05/75] 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 562f0d9054e5ea86b56106832dc00ecff60674a3 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:28:07 -0800 Subject: [PATCH 06/75] [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 e86fafe83f1500aef0b73ee21b7179646fefcc23 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:28:40 -0800 Subject: [PATCH 07/75] 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 fe16a74faabe3932dbd63a05e71c08885464d5b7 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:32:12 -0800 Subject: [PATCH 08/75] 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 af950779a4aa9cfbdaf0366ada5f0fb5489f6625 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:33:25 -0800 Subject: [PATCH 09/75] 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 958537682a6afe08ec655c807303bf6683be84f3 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Dec 2015 19:59:27 -0800 Subject: [PATCH 10/75] [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 550fbfbe696ae0e9ff4ce44068514fbc7bf16fd8 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 26 Dec 2015 00:42:39 -0800 Subject: [PATCH 11/75] 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 3fd472e5948d54d9c3a217adec9eb97521f1a596 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 27 Dec 2015 15:53:48 -0800 Subject: [PATCH 12/75] 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 54bb926c3d1fe2f88d16d281bc014d7ff6c71d6e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 29 Dec 2015 17:35:50 -0800 Subject: [PATCH 13/75] 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 3ea152ed6d61b345a8b61b515987d2dcde53ef72 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 14:32:29 -0800 Subject: [PATCH 14/75] 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 bcd6a56de6a5a2491d60b62badf3a672188a3a7d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 15:38:21 -0800 Subject: [PATCH 15/75] 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 38d2513223c602d8c3a2e41d75396fe8f47b1f21 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 15:49:44 -0800 Subject: [PATCH 16/75] 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 bfaed15c5f4c7d84c0d9a7e6c9144286199f6583 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 30 Dec 2015 16:14:40 -0800 Subject: [PATCH 17/75] 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 1c6e7612995e594312ddb86dde388e8d08e83475 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 31 Dec 2015 17:39:29 -0800 Subject: [PATCH 18/75] 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 9535ffd41ccec6e5970834a8759d02930883fb6d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 31 Dec 2015 17:53:16 -0800 Subject: [PATCH 19/75] 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 c028398b9b7b9e7f164c0e0b0b8dd1a4e159f7ed Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 3 Jan 2016 19:40:49 -0800 Subject: [PATCH 20/75] 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 844f43d271e4fd160627712b9a2d984ccd6cc50b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 4 Jan 2016 14:26:07 -0800 Subject: [PATCH 21/75] 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 8109c65f8d43052a91e426b01a8f7d2bcf1e15eb Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 10:11:10 -0800 Subject: [PATCH 22/75] 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 cbde6b646e566f9d5f8ab920eb3c690783586369 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:05:01 -0800 Subject: [PATCH 23/75] 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 fc4af3921021cefc4048d1b799197ebcd9f53926 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:24:41 -0800 Subject: [PATCH 24/75] 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 0166f7da153d0fcdda8a5247d895295550b9a508 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:26:14 -0800 Subject: [PATCH 25/75] 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 7cb835aac44e34058f3909e5f5f06eb1f0b5c503 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:34:50 -0800 Subject: [PATCH 26/75] 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 9303a264269743af11974e414b0fb5f71f936a2d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 12:46:54 -0800 Subject: [PATCH 27/75] 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 e43b9dc6ed48dd606bdc81f229b59966e271ada8 Mon Sep 17 00:00:00 2001 From: Rex Schrader Date: Tue, 5 Jan 2016 15:50:22 -0800 Subject: [PATCH 28/75] Stand In Line - Improve Clarity --- .../basic-javascript.json | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index e0f3cfd7ec..f80c9ab23c 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -2049,7 +2049,7 @@ "title": "Stand in Line", "description": [ "In Computer Science a queue is an abstract Data Structure where items are kept in order. New items can be added at the back of the queue and old items are taken off from the front of the queue.", - "Write a function queue which takes an \"array\" and an \"item\" as arguments. Add the item onto the end of the array, then remove the first element of the array. The queue function should return the element that was removed." + "Write a function queue which takes an array (arr) and a number (item) as arguments. Add the number to the end of the array, then remove the first element of array. The queue function should then return the element that was removed." ], "releasedOn": "January 1, 2016", "head": [ @@ -2069,32 +2069,33 @@ "capture();" ], "challengeSeed": [ - "// Setup", - "var myArr = [1,2,3,4,5];", - "", "function queue(arr, item) {", " // Your code here", " ", " return item; // Change this line", "}", "", + "// Test Setup", + "var testArr = [1,2,3,4,5];", + "", "// Display Code", - "console.log(\"Before: \" + JSON.stringify(myArr));", - "console.log(queue(myArr, 6)); // Modify this line to test", - "console.log(\"After: \" + JSON.stringify(myArr));" + "console.log(\"Before: \" + JSON.stringify(testArr));", + "console.log(queue(testArr, 6)); // Modify this line to test", + "console.log(\"After: \" + JSON.stringify(testArr));" ], "tail": [ "uncapture();", - "myArr = [1,2,3,4,5];", + "testArr = [1,2,3,4,5];", "(function() { return logOutput.join(\"\\n\");})();" ], "solutions": [ - "var myArr = [ 1,2,3,4,5];\n\nfunction queue(myArr, item) {\n myArr.push(item);\n return myArr.shift();\n}" + "var testArr = [ 1,2,3,4,5];\n\nfunction queue(arr, item) {\n arr.push(item);\n return arr.shift();\n}" ], "tests": [ "assert(queue([],1) === 1, 'message: queue([], 1) should return 1');", "assert(queue([2],1) === 2, 'message: queue([2], 1) should return 2');", - "queue(myArr, 10); assert(myArr[4] === 10, 'message: After queue(myArr, 10), myArr[4] should be 10');" + "assert(queue([5,6,7,8,9],1) === 5, 'message: queue([5,6,7,8,9], 1) should return 5');", + "queue(testArr, 10); assert(testArr[4] === 10, 'message: After queue(testArr, 10), myArr[4] should be 10');" ], "type": "checkpoint", "challengeType": 1 From b6872ee00ef014d1c0116bd039dc4160e9af349b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 17:42:10 -0800 Subject: [PATCH 29/75] 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 b70f301ade5e935a24a0e66567db54bd97763af8 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 17:43:01 -0800 Subject: [PATCH 30/75] 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 f8aa0ec3e5fc312501d53a09d0513aac11aeccb1 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Jan 2016 21:31:48 -0800 Subject: [PATCH 31/75] 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({
); } 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 f02dffaff15105fee6227fbe5cfb52d84d4ec2b2 Mon Sep 17 00:00:00 2001 From: Akira Laine Date: Wed, 6 Jan 2016 17:47:59 +1100 Subject: [PATCH 42/75] created checkpoint: profile-lookup made final changes added release date to Jan 8 made 'prop' argument clearer --- .../basic-javascript.json | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 6e97195c74..90de89b27b 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -3915,6 +3915,76 @@ "type": "waypoint", "challengeType": 1 }, + { + "id": "5688e62ea601b2482ff8422b", + "title": "Profile Lookup", + "description": [ + "We have an array of objects representing different people in our contacts lists.", + "A lookUp function that takes firstName and a property (prop) as arguments has been pre-written for you.", + "The function should check if firstName is an actual contact's firstName and the given property (prop) is a property of that contact.", + "If both are true, then return the \"value\" of that property.", + "If firstName does not correspond to any contacts then return \"No such contact\"", + "If prop does not correspond to any valid properties then return \"No such property\"", + "" + ], + "releasedOn": "January 8, 2016", + "challengeSeed": [ + "//Setup", + "var contacts = [", + " {", + " \"firstName\": \"Akira\",", + " \"lastName\": \"Laine\",", + " \"number\": \"0543236543\",", + " \"likes\": [\"Pizza\", \"Coding\", \"Brownie Points\"]", + " },", + " {", + " \"firstName\": \"Harry\",", + " \"lastName\": \"Potter\",", + " \"number\": \"0994372684\",", + " \"likes\": [\"Hogwarts\", \"Magic\", \"Hagrid\"]", + " },", + " {", + " \"firstName\": \"Sherlock\",", + " \"lastName\": \"Holmes\",", + " \"number\": \"0487345643\",", + " \"likes\": [\"Intruiging Cases\", \"Violin\"]", + " },", + " {", + " \"firstName\": \"Kristian\",", + " \"lastName\": \"Vos\",", + " \"number\": \"unknown\",", + " \"likes\": [\"Javascript\", \"Gaming\", \"Foxes\"]", + " },", + "];", + "", + "", + "function lookUp(firstName, prop){", + "// Only change code below this line", + "", + "// Only change code above this line", + "}", + "", + "// Change these values to test your function", + "lookUp(\"Akira\", \"likes\");" + ], + "solutions": [ + "var contacts = [\n {\n \"firstName\": \"Akira\",\n \"lastName\": \"Laine\",\n \"number\": \"0543236543\",\n \"likes\": [\"Pizza\", \"Coding\", \"Brownie Points\"]\n },\n {\n \"firstName\": \"Harry\",\n \"lastName\": \"Potter\",\n \"number\": \"0994372684\",\n \"likes\": [\"Hogwarts\", \"Magic\", \"Hagrid\"]\n },\n {\n \"firstName\": \"Sherlock\",\n \"lastName\": \"Holmes\",\n \"number\": \"0487345643\",\n \"likes\": [\"Intruiging Cases\", \"Violin\"]\n },\n {\n \"firstName\": \"Kristian\",\n \"lastName\": \"Vos\",\n \"number\": \"unknown\",\n \"likes\": [\"Javascript\", \"Gaming\", \"Foxes\"]\n },\n];\n\n\n//Write your function in between these comments\nfunction lookUp(name, prop){\n for(var i in contacts){\n if(contacts[i].firstName === name) {\n return contacts[i][prop] || \"No such property\";\n }\n }\n return \"No such contact\";\n}\n//Write your function in between these comments\n\nlookUp(\"Akira\", \"likes\");" + ], + "tests": [ + "assert(lookUp('Kristian','lastName') === \"Vos\", 'message: \"Kristian\", \"lastName\" should return \"Vos\"');", + "assert.deepEqual(lookUp(\"Sherlock\", \"likes\"), [\"Intruiging Cases\", \"Violin\"], 'message: \"Sherlock\", \"likes\" should return [\"Intruiging Cases\", \"Violin\"]');", + "assert(typeof lookUp(\"Harry\", \"likes\") === \"object\", 'message: \"Harry\",\"likes\" should return an array');", + "assert(lookUp(\"Bob\", \"number\") === \"No such contact\", 'message: \"Bob\", \"number\" should return \"No such contact\"');", + "assert(lookUp(\"Akira\", \"address\") === \"No such property\", 'message: \"Akira\", \"address\" should return \"No such property\"');" + ], + "type": "checkpoint", + "challengeType": 1, + "nameCn": "", + "nameFr": "", + "nameRu": "", + "nameEs": "", + "namePt": "" + }, { "id": "cf1111c1c11feddfaeb9bdef", "title": "Generate Random Fractions with JavaScript", From 9b91703953ef8a6cd205d9afdeef4098b09451c0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 7 Jan 2016 22:40:51 -0800 Subject: [PATCH 43/75] 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 5f63eb79ee1a2e591c80a2d4bcc52c2159b642d3 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Fri, 8 Jan 2016 07:30:58 +0000 Subject: [PATCH 44/75] closes FreeCodeCamp/FreeCodeCamp#5938 --- .../basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 90de89b27b..bf7551a281 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -2103,7 +2103,7 @@ "
function test(myVal) {
if (myVal > 10) {
return \"Greater Than\";
}
return \"Not Greater Than\";
}
", "If myVal is greater than 10, the function will return \"Greater Than\". If it is not, the function will return \"Not Greater Than\".", "

Instructions

", - "Create an if statement inside the function to return \"Yes\" if testMe is greater than 5. Return \"No\" if it is less than or equal to 5." + "Create an if statement inside the function to return \"Yes\" if testMe is greater than 5 and return \"No\" otherwise." ], "challengeSeed": [ "// Example", From 0e16ab515cb191a6adfc4e9210c1f0b0b9fa4677 Mon Sep 17 00:00:00 2001 From: Logan Tegman Date: Wed, 6 Jan 2016 11:27:03 -0800 Subject: [PATCH 45/75] Fix loop protect thinking default cases are loop labels --- gulpfile.js | 2 +- public/js/lib/loop-protect/loop-protect.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index a8e0329ca2..a73cab824b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -521,7 +521,7 @@ gulp.task('reload', function() { gulp.task('watch', watchDependents, function() { gulp.watch(paths.lessFiles, ['less']); - gulp.watch(paths.js, ['js']); + gulp.watch(paths.js.concat(paths.vendorChallenges), ['js']); gulp.watch(paths.challenges, ['test-challenges', 'reload']); gulp.watch(paths.js, ['js', 'dependents']); gulp.watch( diff --git a/public/js/lib/loop-protect/loop-protect.js b/public/js/lib/loop-protect/loop-protect.js index ac3376b6e1..60031599f5 100644 --- a/public/js/lib/loop-protect/loop-protect.js +++ b/public/js/lib/loop-protect/loop-protect.js @@ -18,7 +18,7 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } // the standard loops - note that recursive is not supported var re = /\b(for|while|do)\b/g; var reSingle = /\b(for|while|do)\b/; - var labelRe = /\b([a-z_]{1}\w+:)/i; + var labelRe = /\b(?!default:)([a-z_]{1}\w+:)/i; var comments = /(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm; var loopTimeout = 1000; @@ -136,7 +136,7 @@ if (typeof DEBUG === 'undefined') { DEBUG = true; } // so that we insert in to the correct location (instead of possibly // outside the logic return line.slice(0, matchPosition) + ';' + method + '({ line: ' + lineNum + ', reset: true }); ' + line.slice(matchPosition); - }; + } if (!offset) { offset = 0; From 5ffa12a77ccf40cfa37f011eedc475d97ce5365b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 8 Jan 2016 10:24:30 -0800 Subject: [PATCH 46/75] 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 a2343829bbd6e27b84c67989edc106888297bcbf Mon Sep 17 00:00:00 2001 From: Harsha Date: Fri, 8 Jan 2016 14:16:18 -0500 Subject: [PATCH 47/75] fixes no url supplied on successful submission --- server/views/stories/submit-story.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/views/stories/submit-story.jade b/server/views/stories/submit-story.jade index 52409cda62..60803e097c 100644 --- a/server/views/stories/submit-story.jade +++ b/server/views/stories/submit-story.jade @@ -35,7 +35,7 @@ .row .form-group - button.btn.btn-big.btn-block.btn-primary#story-submit(type='submit') Submit + button.btn.btn-big.btn-block.btn-primary#story-submit(type='submit', onclick="return false;") Submit script. if (main.storyImage) { $('#image-display').removeClass('hidden-element'); From ef0b1e7801d3490b525b1aff6244bee99d6b4746 Mon Sep 17 00:00:00 2001 From: patsul12 Date: Fri, 8 Jan 2016 13:43:52 -0800 Subject: [PATCH 48/75] changed instructions to be more clear on data type wanted --- .../basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index bf7551a281..2c42dae714 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -3468,7 +3468,7 @@ "
var ourMusic = [
{
\"artist\": \"Daft Punk\",
\"title\": \"Homework\",
\"release_year\": 1997,
\"formats\": [
\"CD\",
\"Cassette\",
\"LP\" ],
\"gold\": true
}
];
", "This is an array of objects and the object has various pieces of metadata about an album. It also has a nested formats array. Additional album records could be added to the top level array.", "

Instructions

", - "Add a new album to the myMusic JSON object. Add artist and title strings, release_year year, and a formats array of strings." + "Add a new album to the myMusic JSON object. Add artist and title strings, release_year number, and a formats array of strings." ], "releasedOn": "January 1, 2016", "challengeSeed": [ From 9c04382a4febebb703c6dc90151903f1eed46be6 Mon Sep 17 00:00:00 2001 From: patsul12 Date: Fri, 8 Jan 2016 16:06:04 -0800 Subject: [PATCH 49/75] updated instructions on label bootstrap waypoint --- .../01-front-end-development-certification/bootstrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/bootstrap.json b/seed/challenges/01-front-end-development-certification/bootstrap.json index 508289633b..d3566748b2 100644 --- a/seed/challenges/01-front-end-development-certification/bootstrap.json +++ b/seed/challenges/01-front-end-development-certification/bootstrap.json @@ -2197,7 +2197,7 @@ "title": "Label Bootstrap Buttons", "description": [ "Just like we labeled our wells, we want to label our buttons.", - "Give each of your button elements text that corresponds to their id." + "Give each of your button elements text that corresponds to its id's selector." ], "tests": [ "assert(new RegExp(\"#target1\",\"gi\").test($(\"#target1\").text()), 'message: Give your button element with the id target1 the text #target1.');", From 392fb281e5cf004ec1814272f84ccf5eee0b6fe9 Mon Sep 17 00:00:00 2001 From: Robert Richey Date: Fri, 8 Jan 2016 18:17:57 -0700 Subject: [PATCH 50/75] Update description for Waypoint: Use a CSS Class to Style an Element Attempting to be more clear with the instructions and use correct terminology. closes #5980 --- .../html5-and-css.json | 1425 +++++------------ 1 file changed, 415 insertions(+), 1010 deletions(-) diff --git a/seed/challenges/01-front-end-development-certification/html5-and-css.json b/seed/challenges/01-front-end-development-certification/html5-and-css.json index a440aa72ad..4aafe3e436 100644 --- a/seed/challenges/01-front-end-development-certification/html5-and-css.json +++ b/seed/challenges/01-front-end-development-certification/html5-and-css.json @@ -5,7 +5,6 @@ "challenges": [ { "id": "bd7123c8c441eddfaeb5bdef", - "title": "Say Hello to HTML Elements", "description": [ "Welcome to Free Code Camp's first coding challenge.", "You can edit code in your text editor, which we've embedded into this web page.", @@ -19,20 +18,14 @@ "Each challenge has tests that you can run at any time by clicking the \"Run tests\" button. Once you get all tests passing, you can advance to the next challenge.", "To pass the test on this challenge, change your h1 element's text to say \"Hello World\" instead of \"Hello\". Then click the \"Run tests\" button." ], - "tests": [ - "assert.isTrue((/hello(\\s)+world/gi).test($('h1').text()), 'message: Your h1 element should have the text \"Hello World\".');" - ], "challengeSeed": [ "

Hello

" ], + "tests": [ + "assert.isTrue((/hello(\\s)+world/gi).test($('h1').text()), 'message: Your h1 element should have the text \"Hello World\".');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Saluda a los Elementos HTML", "descriptionEs": [ "¡Bienvenido/a al primer desafío de programación de Free Code Camp!", @@ -42,9 +35,8 @@ "Cada desafio tiene pruebas que puedes ejecutar en cualquier momento presionado el botón \"Ejecutar pruebas\". Una vez logres pasar todas las pruebas, podrás avanzar al siguiente desafio.", "Para pasar la prueba en este desafio, cambia tu texto de la etiqueta h1 para que diga \"Hello World\" en lugar de \"Hello\". Entonces presiona el botón \"Ejecutar pruebas\"." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Begrüße die HTML Elemente", + "title": "Say Hello to HTML Elements", "descriptionDe": [ "Willkommen bei der ersten Programmier-Challenge von Free Code Camp! Klicke auf den folgenden Button für weitere Instruktionen.", "Sehr gut. Jetzt kannst du den Rest der Instruktionen für diese Challenge lesen.", @@ -57,7 +49,6 @@ }, { "id": "bad87fee1348bd9aedf0887a", - "title": "Headline with the h2 Element", "description": [ "Over the next few challenges, we'll build an HTML5 app that will look something like this:", "\"A", @@ -66,23 +57,17 @@ "h2 elements are slightly smaller than h1 elements. There are also h3, h4, h5 and h6 elements.", "Add an h2 tag that says \"CatPhotoApp\" to create a second HTML element below your \"Hello World\" h1 element." ], + "challengeSeed": [ + "

Hello World

" + ], "tests": [ "assert(($(\"h2\").length > 0), 'message: Create an h2 element.');", "assert(code.match(/<\\/h2>/g) && code.match(/<\\/h2>/g).length === code.match(/

/g).length, 'message: Make sure your h2 element has a closing tag.');", "assert.isTrue((/cat(\\s)?photo(\\s)?app/gi).test($(\"h2\").text()), 'message: Your h2 element should have the text \"CatPhotoApp\".');", "assert.isTrue((/hello(\\s)+world/gi).test($(\"h1\").text()), 'message: Your h1 element should have the text \"Hello World\".');" ], - "challengeSeed": [ - "

Hello World

" - ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Encabezado con el elemento h2", "descriptionEs": [ "Durante los siguientes desafios, construiremos una aplicación HTML que lucirá como la siguiente:", @@ -92,9 +77,8 @@ "Los elementos h2 son ligeramente más pequeños que los elementos h1. También hay elementos h3, h4, h5 y h6", "Agrega una etiqueta h2 que diga \"CatPhotoApp\" para crear un segundo elemento HTML debajo de tu elemento h1 \"Hello World\"." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Überschrift mit dem h2 Element", + "title": "Headline with the h2 Element", "descriptionDe": [ "Füge unter h1 \"Hello World\" ein zweites HTML Element h2 hinzu, in dem \"CatPhotoApp\" steht.", "Das eingetragene h2 Element wird ein h2 Element auf der Website erzeugen.", @@ -104,39 +88,31 @@ }, { "id": "bad87fee1348bd9aedf08801", - "title": "Inform with the Paragraph Element", "description": [ "p elements are the preferred element for normal-sized paragraph text on websites. P is short for \"paragraph\".", "You can create a p element like this:", "<p>I'm a p tag!</p>", "Create a p element below your h2 element, and give it the text \"Hello Paragraph\"." ], + "challengeSeed": [ + "

Hello World

", + "

CatPhotoApp

" + ], "tests": [ "assert(($(\"p\").length > 0), 'message: Create a p element.');", "assert.isTrue((/hello(\\s)+paragraph/gi).test($(\"p\").text()), 'message: Your p element should have the text \"Hello Paragraph\".');", "assert(code.match(/<\\/p>/g) && code.match(/<\\/p>/g).length === code.match(/

p element has a closing tag.');" ], - "challengeSeed": [ - "

Hello World

", - "

CatPhotoApp

" - ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Informa con el Elemento Párrafo", "descriptionEs": [ "Los elementos p son los elementos preferidos en los sitios web para los párrafos de texto en tamaño normal. La P es abreviatura de \"párrafo\".", "Tú puedes crear un elemento párrafo como éste: <p>¡Soy una etiqueta p!</p>", "Crea un elemento p debajo de tu elemento h2, y ponle el texto \"Hello Paragraph\"." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Informiere mit dem Paragraph Element", + "title": "Inform with the Paragraph Element", "descriptionDe": [ "Erstelle ein p Element unter deinem h2 Element und füge den Text \"Hello Paragraph\" ein.", "p Elemente sind das bevorzugte Element für normalen Paragraphen-Text auf einer Website. P ist die Abkürzung für \"Paragraph\".", @@ -145,19 +121,12 @@ }, { "id": "bad87fee1348bd9aedf08802", - "title": "Uncomment HTML", "description": [ "Commenting is a way that you can leave comments within your code without affecting the code itself.", "Commenting is also a convenient way to make code inactive without having to delete it entirely.", "You can start a comment with <!-- and end a comment with -->", "Uncomment your h1, h2 and p elements." ], - "tests": [ - "assert($(\"h1\").length > 0, 'message: Make your h1 element visible on your page by uncommenting it.');", - "assert($(\"h2\").length > 0, 'message: Make your h2 element visible on your page by uncommenting it.');", - "assert($(\"p\").length > 0, 'message: Make your p element visible on your page by uncommenting it.');", - "assert(!/-->/gi.test(code), 'message: Be sure to delete all trailing comment tags, i.e. -->.');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"h1\").length > 0, 'message: Make your h1 element visible on your page by uncommenting it.');", + "assert($(\"h2\").length > 0, 'message: Make your h2 element visible on your page by uncommenting it.');", + "assert($(\"p\").length > 0, 'message: Make your p element visible on your page by uncommenting it.');", + "assert(!/-->/gi.test(code), 'message: Be sure to delete all trailing comment tags, i.e. -->.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Quita comentarios HTML", "descriptionEs": [ "\"Comentar\" es una manera en la que puedes dejar anotaciones en tu código sin afectar el código mismo.", @@ -182,9 +151,8 @@ "Puedes comenzar un comentario con <!-- y terminar de comentar con -->", "Quita el comentario a los elementos h1, h2 y p" ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: HTML entkommentieren", + "title": "Uncomment HTML", "descriptionDe": [ "Entkommentiere deine h1, h2 und p Elemente.", "Kommentieren erlaubt dir Kommentare innerhalb des Codes zu hinterlassen, ohne diesen selbst zu beeinflussen.", @@ -194,18 +162,11 @@ }, { "id": "bad87fee1348bd9aedf08804", - "title": "Comment out HTML", "description": [ "Remember that in order to start a comment, you need to use <!-- and to end a comment, you need to use -->", "Here you'll need to end the comment before your h2 element begins.", "Comment out your h1 element and your p element, but leave your h2 element uncommented." ], - "tests": [ - "assert(($(\"h1\").length === 0), 'message: Comment out your h1 element so that it is not visible on your page.');", - "assert(($(\"h2\").length > 0), 'message: Leave your h2 element uncommented so that it is visible on your page.');", - "assert(($(\"p\").length === 0), 'message: Comment out your p element so that it is not visible on your page.');", - "assert(code.match(/-->/g).length > 1, 'message: Be sure to close each of your comments with -->.');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert(($(\"h1\").length === 0), 'message: Comment out your h1 element so that it is not visible on your page.');", + "assert(($(\"h2\").length > 0), 'message: Leave your h2 element uncommented so that it is visible on your page.');", + "assert(($(\"p\").length === 0), 'message: Comment out your p element so that it is not visible on your page.');", + "assert(code.match(/-->/g).length > 1, 'message: Be sure to close each of your comments with -->.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Comenta en HTML", "descriptionEs": [ "Recuerda que para comenzar un comentario, necesitas usar <!-- y para terminar un comentario, necesitas usar -->", "Aquí necesitarás terminar el comentario antes que comience el elemento h2.", "Comenta el elemento h1 y el elemento p, pero deja sin comentar el elemento h2" ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: HTML auskommentieren", + "title": "Comment out HTML", "descriptionDe": [ "Kommentiere die Elemente h1 und p aus, aber lasse dein h2 Element unkommentiert.", "Denk daran, dass du einen Kommentar mit <!-- anfangen und mit --> wieder beenden kannst.", @@ -240,16 +200,12 @@ }, { "id": "bad87fee1348bd9aedf08833", - "title": "Fill in the Blank with Placeholder Text", "description": [ "Web developers traditionally use lorem ipsum text as placeholder text. It's called lorem ipsum text because those are the first two words of a famous passage by Cicero of Ancient Rome.", "Lorem ipsum text has been used as placeholder text by typesetters since the 16th century, and this tradition continues on the web.", "Well, 5 centuries is long enough. Since we're building a CatPhotoApp, let's use something called kitty ipsum text.", "Replace the text inside your p element with the first few words of this kitty ipsum text: Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff." ], - "tests": [ - "assert.isTrue((/Kitty(\\s)+ipsum(\\s)+dolor/gi).test($(\"p\").text()), 'message: Your p element should contain the first few words of the provided kitty ipsum text.');" - ], "challengeSeed": [ "

Hello World

", "", @@ -257,14 +213,11 @@ "", "

Hello Paragraph

" ], + "tests": [ + "assert.isTrue((/Kitty(\\s)+ipsum(\\s)+dolor/gi).test($(\"p\").text()), 'message: Your p element should contain the first few words of the provided kitty ipsum text.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Llena espacios con texto de relleno", "descriptionEs": [ "Los desarrolladores web tradicionalmente usan Lorem Ipsum como texto de relleno. Se llama texto Lorem Ipsum porque esas son las primeras dos palabras de una cita famosa de Cicerón de la Roma Antigua.", @@ -272,9 +225,8 @@ "Bueno, 5 siglos es bastante. Ya que estamos construyendo una aplicación de fotos de gatos (CatPhotoApp), ¡usemos algo llamado Kitty Ipsum!", "Remplaza el texto dentro de tu elemento p con las primeras palabras de este texto kitty ipsum: Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Fülle die Lücken mit Platzhalter-Text", + "title": "Fill in the Blank with Placeholder Text", "descriptionDe": [ "Ersetze den Text in deinem p Element mit den ersten Wörtern des zur Verfügung gestellten \"Kitty Ipsum\" Textes.", "Webentwickler nutzen für gewöhnlich \"Lorem Ipsum\" Text als Platzhalter. Es heißt \"Lorem Ipsum\", weil es die ersten zwei Wörter aus einer bekannten Passage von Cicero des alten Roms sind.", @@ -285,17 +237,11 @@ }, { "id": "bad87fed1348bd9aedf08833", - "title": "Delete HTML Elements", "description": [ "Our phone doesn't have much vertical space.", "Let's remove the unnecessary elements so we can start building our CatPhotoApp.", "Delete your h1 element so we can simplify our view." ], - "tests": [ - "assert(($(\"h1\").length == 0), 'message: Delete your h1 element.');", - "assert(($(\"h2\").length > 0), 'message: Leave your h2 element on the page.');", - "assert(($(\"p\").length > 0), 'message: Leave your p element on the page.');" - ], "challengeSeed": [ "

Hello World

", "", @@ -303,23 +249,21 @@ "", "

Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.

" ], + "tests": [ + "assert(($(\"h1\").length == 0), 'message: Delete your h1 element.');", + "assert(($(\"h2\").length > 0), 'message: Leave your h2 element on the page.');", + "assert(($(\"p\").length > 0), 'message: Leave your p element on the page.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Borra elementos HTML", "descriptionEs": [ "Nuestro teléfono no tiene mucho espacio vertical.", "Eliminemos los elementos innecesarios para que empecemos a construir nuestra CatPhotoApp.", "Borra el elemento h1 para simplificar nuestra vista." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Entferne HTML Elemente", + "title": "Delete HTML Elements", "descriptionDe": [ "Lösche die Elemente h1, damit wir etwas Ordnung schaffen.", "Unser Smartphone hat nicht sehr viel vertikalen Raum.", @@ -328,7 +272,6 @@ }, { "id": "bad87fee1348bd9aedf08803", - "title": "Change the Color of Text", "description": [ "Now let's change the color of some of our text.", "We can do this by changing the style of your h2 element.", @@ -337,22 +280,16 @@ "<h2 style=\"color: blue\">CatPhotoApp</h2>", "Change your h2 element's style so that its text color is red." ], - "tests": [ - "assert($(\"h2\").css(\"color\") === \"rgb(255, 0, 0)\", 'message: Your h2 element should be red.');" - ], "challengeSeed": [ "

CatPhotoApp

", "", "

Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.

" ], + "tests": [ + "assert($(\"h2\").css(\"color\") === \"rgb(255, 0, 0)\", 'message: Your h2 element should be red.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Cambia el color del texto", "descriptionEs": [ "Ahora cambiemos el color de parte de nuestro texto.", @@ -362,9 +299,8 @@ "<h2 style=\"color: blue\">CatPhotoApp</h2>", "Cambia el estilo del elemento h2 de manera que el color de su texto sea rojo." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Ändere die Farbe des Textes", + "title": "Change the Color of Text", "descriptionDe": [ "Ändere den Style des h2 Elements, damit die Textfarbe Rot ist.", "Wir können das bewerkstelligen, indem wir den \"style\" des h2 Elements ändern.", @@ -373,7 +309,6 @@ }, { "id": "bad87fee1348bd9aedf08805", - "title": "Use CSS Selectors to Style Elements", "description": [ "With CSS, there are hundreds of CSS properties that you can use to change the way an element looks on your page.", "When you entered <h2 style=\"color: red\">CatPhotoApp</h2>, you were giving that individual h2 element an inline style.", @@ -388,6 +323,11 @@ "Note that it's important to have both opening and closing curly braces ({ and }) around each element's style. You also need to make sure your element's style is between the opening and closing style tags. Finally, be sure to add the semicolon to the end of each of your element's styles.", "Delete your h2 element's style attribute and instead create a CSS style element. Add the necessary CSS to turn all h2 elements blue." ], + "challengeSeed": [ + "

CatPhotoApp

", + "", + "

Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.

" + ], "tests": [ "assert(!$(\"h2\").attr(\"style\"), 'message: Remove the style attribute from your h2 element.');", "assert($(\"style\") && $(\"style\").length > 1, 'message: Create a style element.');", @@ -395,19 +335,8 @@ "assert(code.match(/h2\\s*\\{\\s*color\\s*:.*;\\s*\\}/g), 'message: Ensure that your stylesheet h2 declaration is valid with a semicolon and closing brace.');", "assert(code.match(/<\\/style>/g) && code.match(/<\\/style>/g).length === (code.match(//g) || []).length, 'message: Make sure all your style elements are valid and have a closing tag.');" ], - "challengeSeed": [ - "

CatPhotoApp

", - "", - "

Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.

" - ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa selectores CSS para dar estilo a los elementos", "descriptionEs": [ "Con CSS, hay cientos de propiedades CSS que puedes usar para cambiar como un elemento se ve en una página web.", @@ -421,9 +350,8 @@ "Fíjate que es importante tener llaves de apertura y de cierre ({ y }) alrededor del estilo para cada elemento. También necesitas asegurarte que el estilo para tu elemento esté entre las etiquetas style de apertura y cierre. Finalmente, asegúrate de agregar el punto y coma al final de cada uno de los estilos de tu elemento.", "Borra el atributo style de tu elemento h2 y a cambio escribe un elemento style CSS. Agrea el CSS necesario para hacer todos los elementos h2 de color azul." ], - "namePt": "", - "descriptionPt": [], "nameDe": "Waypoint: Nutze CSS Selektoren um Elemente zu gestalten", + "title": "Use CSS Selectors to Style Elements", "descriptionDe": [ "Lösche das Style Attribute deines h2 Elements und erstelle stattdessen ein CSS style Element. Füge das notwendige CSS hinzu, um alle h2 Elemente Blau zu färben.", "CSS liefert dir hunderte Attribute oder \"attributes\" um HTML Elemente auf deiner Seite zu gestalten.", @@ -436,7 +364,6 @@ }, { "id": "bad87fee1348bd9aecf08806", - "title": "Use a CSS Class to Style an Element", "description": [ "Classes are reusable styles that can be added to HTML elements.", "Here's an example CSS class declaration:", @@ -449,14 +376,8 @@ "You can apply a class to an HTML element like this:", "<h2 class=\"blue-text\">CatPhotoApp</h2>", "Note that in your CSS style element, classes should start with a period. In your HTML elements' class declarations, classes shouldn't start with a period.", - "Instead of creating a new style element, try removing the h2 style declaration from your existing style element, then replace it with the class declaration for .red-text", - "Create a CSS class called red-text and apply it to your h2 element." - ], - "tests": [ - "assert($(\"h2\").css(\"color\") === \"rgb(255, 0, 0)\", 'message: Your h2 element should be red.');", - "assert($(\"h2\").hasClass(\"red-text\"), 'message: Your h2 element should have the class red-text.');", - "assert(code.match(/\\.red-text\\s*\\{\\s*color:\\s*red;\\s*\\}/g), 'message: Your stylesheet should declare a red-text class and have its color set to red.');", - "assert($(\"h2\").attr(\"style\") === undefined, 'message: Do not use inline style declarations like style=\"color: red\" in your h2 element.');" + "Inside your style element, change the h2 selector to .red-text and update the color's value from blue to red.", + "Give your h2 element the class attribute with a value of 'red-text'." ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 0)\", 'message: Give your body element the background-color of black.');", + "assert(code.match(/" + ], "tests": [ "assert(($(\"h1\").length > 0), 'message: Create an h1 element.');", "assert(($(\"h1\").length > 0 && $(\"h1\").text().match(/hello world/i)), 'message: Your h1 element should have the text Hello World.');", @@ -3456,22 +3080,8 @@ "assert(($(\"h1\").length > 0 && $(\"h1\").css(\"font-family\").match(/monospace/i)), 'message: Your h1 element should inherit the font Monospace from your body element.');", "assert(($(\"h1\").length > 0 && $(\"h1\").css(\"color\") === \"rgb(0, 128, 0)\"), 'message: Your h1 element should inherit the color green from your body element.');" ], - "challengeSeed": [ - "" - ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Hereda estilos del elemento cuerpo", "descriptionEs": [ "Ya hemos demostrado que cada página HTML tiene un cuerpo (body), y que puede dársele estilo CSS.", @@ -3479,11 +3089,7 @@ "En primer lugar, crea un elemento h1 con el texto Hello World", "Después, vamos a darle a todos los elementos de tu página el color verde (green) añadiendo color: green; a la declaración de estilo de tu elemento body.", "Por último, da a tu elemento body el tipo de letra Monospace añadiendo font-family: Monospace; a la declaración del estilo de tu elemento body." - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08756", @@ -3495,10 +3101,6 @@ "Create a CSS class called pink-text that gives an element the color pink.", "Give your h1 element the class of pink-text." ], - "tests": [ - "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", - "assert($(\"h1\").css(\"color\") === \"rgb(255, 192, 203)\", 'message: Your h1 element should be pink.');" - ], "challengeSeed": [ "", "

Hello World!

" ], + "tests": [ + "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", + "assert($(\"h1\").css(\"color\") === \"rgb(255, 192, 203)\", 'message: Your h1 element should be pink.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Priorizar un estilo sobre otro", "descriptionEs": [ "A veces los elementos HTML recibirán múltiples estilos que entran en conflicto entre sí.", @@ -3524,11 +3124,7 @@ "Vamos a ver lo que sucede cuando creamos una clase que hace rosado el texto y luego lo aplicamos a un elemento. ¿Anulará (override) nuestra clase la propiedad CSS color: green del elemento body?", "Crea una clase CSS llamada pink-text que le da a un elemento el color rosado.", "Ponle a tu elemento h1 la clase de pink-text." - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf04756", @@ -3542,12 +3138,6 @@ "class=\"class1 class2\"", "Note: It doesn't matter which order the classes are listed in." ], - "tests": [ - "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", - "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", - "assert($(\".pink-text\").hasClass(\"blue-text\"), 'message: Both blue-text and pink-text should belong to the same h1 element.');", - "assert($(\"h1\").css(\"color\") === \"rgb(0, 0, 255)\", 'message: Your h1 element should be blue.');" - ], "challengeSeed": [ "", "

Hello World!

" ], + "tests": [ + "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", + "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", + "assert($(\".pink-text\").hasClass(\"blue-text\"), 'message: Both blue-text and pink-text should belong to the same h1 element.');", + "assert($(\"h1\").css(\"color\") === \"rgb(0, 0, 255)\", 'message: Your h1 element should be blue.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Anula estilos con CSS posterior", "descriptionEs": [ "¡Nuestra clase \"pink-text\" anuló la declaración CSS de nuestro elemento body!", @@ -3578,11 +3168,7 @@ "La aplicación de múltiples atributos de clase a un elemento HTML se hace usando espacios entre ellos así:", "class=\"class1 class2\"", "Nota: No importa lo que ordenan las clases se enumeran en el." - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd8aedf06756", @@ -3600,14 +3186,6 @@ "}", "Note: It doesn't matter whether you declare this css above or below pink-text class, since id attribute will always take precedence." ], - "tests": [ - "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", - "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", - "assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Give your h1 element the id of orange-text.');", - "assert(code.match(/#orange-text\\s*{/gi), 'message: Create a CSS declaration for your orange-text id');", - "assert(!code.match(//gi), 'message: Do not give your h1 any style attributes.');", - "assert($(\"h1\").css(\"color\") === \"rgb(255, 165, 0)\", 'message: Your h1 element should be orange.');" - ], "challengeSeed": [ "", "

Hello World!

" ], + "tests": [ + "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", + "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", + "assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Give your h1 element the id of orange-text.');", + "assert(code.match(/#orange-text\\s*{/gi), 'message: Create a CSS declaration for your orange-text id');", + "assert(!code.match(//gi), 'message: Do not give your h1 any style attributes.');", + "assert($(\"h1\").css(\"color\") === \"rgb(255, 165, 0)\", 'message: Your h1 element should be orange.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Anula la declaración de clases dando estilo a los atributos ID", "descriptionEs": [ "Acabamos de demostrar que los navegadores leen CSS de arriba hacia abajo. Eso significa que, en el caso de un conflicto, el navegador utilizará la última declaración CSS. ", @@ -3645,11 +3225,7 @@ "  color: brown;", "}", "Nota: No importa si usted declara este css encima o debajo de la clase de texto de color rosa, ya atributo id siempre tendrá prioridad." - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf06756", @@ -3661,13 +3237,6 @@ "<h1 style=\"color: green\">", "Leave the blue-text and pink-text classes on your h1 element." ], - "tests": [ - "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", - "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", - "assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Your h1 element should have the id of orange-text.');", - "assert(code.match(/h1 element the inline style of color: white.');", - "assert($(\"h1\").css(\"color\") === \"rgb(255, 255, 255)\", 'message: Your h1 element should be white.');" - ], "challengeSeed": [ "", "

Hello World!

" ], + "tests": [ + "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", + "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", + "assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Your h1 element should have the id of orange-text.');", + "assert(code.match(/h1 element the inline style of color: white.');", + "assert($(\"h1\").css(\"color\") === \"rgb(255, 255, 255)\", 'message: Your h1 element should be white.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Anula declaraciones de clase con estilos en línea", "descriptionEs": [ "Así que hemos demostrado que las declaraciones de identificadores anulan las declaraciones de clase, independientemente del lugar donde se declaran en tu elemento de estilo CSS style.", @@ -3702,11 +3272,7 @@ "Utiliza un estilo en línea para tratar de hacer blanco nuestro elemento h1. Recuerda, los estilos en línea se ven así: ", "<h1 style=\"color: green\">", "Deja las clases blue-text y pink-text en tu elemento h1." - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf07756", @@ -3720,14 +3286,6 @@ "An example of how to do this is:", "color: red !important;" ], - "tests": [ - "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", - "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", - "assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Your h1 element should have the id of orange-text.');", - "assert(code.match(/h1 element should have the inline style of color: white.');", - "assert(code.match(/pink.*\\!important;/gi), 'message: Your pink-text class should have the !important keyword to override all other declarations.');", - "assert($(\"h1\").css(\"color\") === \"rgb(255, 192, 203)\", 'message: Your h1 element should be pink.');" - ], "challengeSeed": [ "", "

Hello World!

" ], + "tests": [ + "assert($(\"h1\").hasClass(\"pink-text\"), 'message: Your h1 element should have the class pink-text.');", + "assert($(\"h1\").hasClass(\"blue-text\"), 'message: Your h1 element should have the class blue-text.');", + "assert($(\"h1\").attr(\"id\") === \"orange-text\", 'message: Your h1 element should have the id of orange-text.');", + "assert(code.match(/h1 element should have the inline style of color: white.');", + "assert(code.match(/pink.*\\!important;/gi), 'message: Your pink-text class should have the !important keyword to override all other declarations.');", + "assert($(\"h1\").css(\"color\") === \"rgb(255, 192, 203)\", 'message: Your h1 element should be pink.');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Anula todos los demás estilos utilizando Important", "descriptionEs": [ "¡Hurra! Demostramos que los estilos en línea anularán todas las declaraciones CSS de tu elemento style. ", @@ -3764,11 +3324,7 @@ "Vamos a añadir la palabra clave !important a tu declaración del color de pink-text para estar 100% seguros que tu elemento h1 será rosado.", "Un ejemplo de cómo hacer esto es:", "color: red !important;" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08726", @@ -3779,10 +3335,6 @@ "With CSS, we use 6 hexadecimal numbers to represent colors. For example, #000000 is the lowest possible value, and it represents the color black.", "Replace the word black in our body element's background-color with its hex code representation, #000000." ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 0)\", 'message: Give your body element the background-color of black.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#000(000)?((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color black instead of the word black. For example body { color: #000000; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 0)\", 'message: Give your body element the background-color of black.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#000(000)?((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color black instead of the word black. For example body { color: #000000; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para especificar colores", "descriptionEs": [ "¿Sabías que hay otras maneras de representar los colores en CSS? Una de estas formas es llamada código hexadecimal o código hex para abreviar. ", "El sistema Decimal se refiere al que nos permite representar cantidades empleando los dígitos del cero al nueve - los números que la gente usa en la vida cotidiana. El sistema Hexadecimal incluye estos 10 dígitos más las letras A, B, C, D, E y F. Esto significa que Hexadecimal tiene un total de 16 dígitos posibles, en lugar de las 10 posibles que nos da nuestro sistema numérico normal en base 10. ", "Con CSS, utilizamos 6 dígitos hexadecimales para representar los colores. Por ejemplo, #000000 es el valor más bajo posible, y representa el color negro. ", "Reemplaza la palabra black en el color de fondo (background-color) de nuestro elemento body por su representación hexadecimal #000000" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08725", @@ -3818,10 +3364,6 @@ "F is the highest number in hex code, and it represents the maximum possible brightness.", "Let's turn our body element's background-color white by changing its hex code to #FFFFFF" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 255, 255)\", 'message: Your body element should have the background-color of white.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#FFF(FFF)?((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color white instead of the word white. For example body { color: #FFFFFF; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 255, 255)\", 'message: Your body element should have the background-color of white.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#FFF(FFF)?((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color white instead of the word white. For example body { color: #FFFFFF; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para colorear de blanco los elementos", "descriptionEs": [ "0 es el dígito más bajo en código hexadecimal, y representa una completa ausencia de color.", "F es el dígito más alto en código hexadecimal, y representa el máximo brillo posible.", "Volvamos blanco el color de fondo (background-color) de nuestro elemento body, cambiando su código hexadecimal por #FFFFFF" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08724", @@ -3858,10 +3394,6 @@ "So to get the absolute brightest red, you would just use F for the first and second digits (the highest possible value) and 0 for the third, fourth, fifth and sixth digits (the lowest possible value).", "Make the body element's background color red by giving it the hex code value of #FF0000" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 0, 0)\", 'message: Give your body element the background-color of red.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#((F00)|(FF0000))((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color red instead of the word red. For example body { color: #FF0000; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 0, 0)\", 'message: Give your body element the background-color of red.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#((F00)|(FF0000))((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color red instead of the word red. For example body { color: #FF0000; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para colorear de rojo los elementos", "descriptionEs": [ "Te puedes estar preguntando por qué usamos 6 dígitos para representar un color en lugar de sólo uno o dos. La respuesta es que el uso de 6 dígitos nos da una enorme variedad. ", @@ -3884,11 +3414,7 @@ "Los códigos hexadecimales siguen el formato rojo-verde-azul (red-green-blue) o formato rgb. Los dos primeros dígitos del código hexadecimal representan la cantidad de rojo en el color. El tercer y cuarto dígitos representan la cantidad de verde. El quinto y sexto representan la cantidad de azul .", "Así que para conseguir el rojo absolutamente más brillante, basta que uses F para el primer y segundo dígitos (el dígito más alto posible) y 0 para el tercero, cuarto, quinto y sexto dígitos (el dígito más bajo posible).", "Haz que el color de fondo (background-color) del elemento body sea rojo dándole el código hexadecimal #FF0000" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08723", @@ -3898,10 +3424,6 @@ "So to get the absolute brightest green, you would just use F for the third and fourth digits (the highest possible value) and 0 for all the other digits (the lowest possible value).", "Make the body element's background color green by giving it the hex code value of #00FF00" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(0, 255, 0)\", 'message: Give your body element the background-color of green.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#((0F0)|(00FF00))((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color green instead of the word green. For example body { color: #00FF00; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 255, 0)\", 'message: Give your body element the background-color of green.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#((0F0)|(00FF00))((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color green instead of the word green. For example body { color: #00FF00; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para colorear de verde los elementos", "descriptionEs": [ "Recuerda que el código hexadecimal sigue el formato rojo-verde-azul o rgb. Los dos primeros dígitos del código hexadecimal representan la cantidad de rojo en el color. El tercer y cuarto dígitos representan la cantidad de verde. El quinto y sexto representar la cantidad de azul.", "Así que para conseguir el verde absoluto más brillante, sólo usas F en el tercer y cuarto dígitos (el dígito más alto posible) y 0 para todos los otros dígitos (el dígito más bajo posible). ", "Haz que el color de fondo (background-color) del elemento body sea verde, dándole el código hexadecimal #00FF00" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08722", @@ -3936,10 +3452,6 @@ "So to get the absolute brightest blue, we use F for the fifth and sixth digits (the highest possible value) and 0 for all the other digits (the lowest possible value).", "Make the body element's background color blue by giving it the hex code value of #0000FF" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 255)\", 'message: Give your body element the background-color of blue.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#((00F)|(0000FF))((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color blue instead of the word blue. For example body { color: #0000FF; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 255)\", 'message: Give your body element the background-color of blue.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#((00F)|(0000FF))((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color blue instead of the word blue. For example body { color: #0000FF; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para colorear de azul los elementos", "descriptionEs": [ "Los códigos hexadecimales siguen el formato rojo-verde-azul o rgb. Los dos primeros dígitos del código hexadecimal representan la cantidad de rojo en el color. El tercer y cuarto dígitos representan la cantidad de verde. El quinto y sexto representar la cantidad de azul .", "Así que para conseguir el azul absoluto más brillante, utilizamos F para la quinta y sexta cifras (el dígito más alto posible) y 0 para todos los otros dígitos (el dígito más bajo posible ). ", "Haz que el color de fondo (background-color) del elemento body sea azul, dándole el código hexadecimal #0000FF" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08721", @@ -3974,10 +3480,6 @@ "For example, orange is pure red, mixed with some green, and no blue.", "Make the body element's background color orange by giving it the hex code value of #FFA500" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 165, 0)\", 'message: Give your body element the background-color of orange.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#FFA500((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color orange instead of the word orange. For example body { color: #FFA500; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 165, 0)\", 'message: Give your body element the background-color of orange.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#FFA500((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code for the color orange instead of the word orange. For example body { color: #FFA500; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa código hex para mezclar colores", "descriptionEs": [ "A partir de estos tres colores puros (rojo, verde y azul), podemos crear 16 millones de colores.", "Por ejemplo, el naranja es rojo puro, mezclado con un poco de verde, y sin azul.", "Haz que el color de fondo del elemento body sea anaranjado, dándole el código hexadecimal #FFA500" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aede08720", @@ -4012,10 +3508,6 @@ "We can also create different shades of gray by evenly mixing all three colors.", "Make the body element's background color gray by giving it the hex code value of #808080" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(128, 128, 128)\", 'message: Give your body element the background-color of gray.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#808080((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code the color gray instead of the word gray. For example body { color: #808080; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(128, 128, 128)\", 'message: Give your body element the background-color of gray.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#808080((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use the hex code the color gray instead of the word gray. For example body { color: #808080; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para colorear de gris los elementos", "descriptionEs": [ "A partir de estos tres colores puros (rojo, verde y azul), podemos crear 16 millones de colores.", "También podemos crear diferentes tonos de gris mezclando uniformemente los tres colores.", "Haz que el color de fondo del elemento body sea gris, dándole el código hexadecimal #808080" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08720", @@ -4049,10 +3535,6 @@ "We can also create other shades of gray by evenly mixing all three colors. We can go very close to true black.", "Make the body element's background color a dark gray by giving it the hex code value of #111111" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(17, 17, 17)\", 'message: Give your body element the background-color of a dark gray.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#111(111)?((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use hex code to make a dark gray. For example body { color: #111111; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(17, 17, 17)\", 'message: Give your body element the background-color of a dark gray.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#111(111)?((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use hex code to make a dark gray. For example body { color: #111111; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa el código hexadecimal para colorear con tonos grises", "descriptionEs": [ "También podemos crear otros tonos de gris mezclando uniformemente los tres colores. Podemos ir muy cerca del verdadero negro. ", "Haz que el color de fondo del elemento body sea gris oscuro dandole el código hexadecimal #111111" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aedf08719", @@ -4087,10 +3563,6 @@ "This reduces the total number of possible colors to around 4,000. But browsers will interpret #FF0000 and #F00 as exactly the same color.", "Go ahead, try using #F00 to turn the body element's background-color red." ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 0, 0)\", 'message: Give your body element the background-color of red.');", - "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#F00((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use abbreviated hex code instead of a six-character hex code. For example body { color: #F00; }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 0, 0)\", 'message: Give your body element the background-color of red.');", + "assert(code.match(/body\\s*{(([\\s\\S]*;\\s*?)|\\s*?)background.*\\s*:\\s*?#F00((\\s*})|(;[\\s\\S]*?}))/gi), 'message: Use abbreviated hex code instead of a six-character hex code. For example body { color: #F00; }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Uso código hex abreviado", "descriptionEs": [ "Mucha gente se siente abrumada por las posibilidades de más de 16 millones de colores. Y es difícil recordar el código hexadecimal. Afortunadamente puedes acortarlo. ", "Por ejemplo, el rojo, que es #FF0000 en código hexadecimal, se puede abreviar a #F00. Es decir, un dígito para el rojo, un dígito para el verde, un dígito para el azul. ", "Esto reduce el número total de posibles colores a alrededor de 4.000. Pero los navegadores interpretarán #FF0000 y #F00 como exactamente el mismo color. ", "Adelante, intente usar #F00 para volver rojo el color de fondo del elemento body." - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad87fee1348bd9aede08718", @@ -4131,10 +3597,6 @@ "If you do the math, 16 times 16 is 256 total values. So rgb, which starts counting from zero, has the exact same number of possible values as hex code.", "Let's replace the hex code in our body element's background color with the RGB value for black: rgb(0, 0, 0)" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 0)\", 'message: Your body element should have a black background.');", - "assert(code.match(/rgb\\s*\\(\\s*0\\s*,\\s*0\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of black. For example body { background-color: rgb(0, 0, 0); }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 0)\", 'message: Your body element should have a black background.');", + "assert(code.match(/rgb\\s*\\(\\s*0\\s*,\\s*0\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of black. For example body { background-color: rgb(0, 0, 0); }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa RGB para colorear elementos", "descriptionEs": [ "Otra forma en la que puedes representar colores en CSS es usando valores rgb.", @@ -4160,12 +3620,7 @@ "En lugar de utilizar seis dígitos hexadecimales, con rgb especificas el brillo de cada color con un número entre 0 y 255.", "Si haces la matemática, 16 veces 16 es 256 valores totales. Así que rgb, que comienza a contar desde cero, tiene exactamente el mismo número de valores posibles que el código hexadecimal.", "Remplacemos el código hexadecimal del color de fondo de nuestro elemento body por el valor RGB para el negro: rgb(0, 0, 0)" - ], - - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad88fee1348bd9aedf08726", @@ -4178,10 +3633,6 @@ "Instead of using six hexadecimal digits like you do with hex code, with rgb you specify the brightness of each color with a number between 0 and 255.", "Change the body element's background color from the RGB value for black to the rgb value for white: rgb(255, 255, 255)" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 255, 255)\", 'message: Your body should have a white background.');", - "assert(code.match(/rgb\\s*\\(\\s*255\\s*,\\s*255\\s*,\\s*255\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of white. For example body { background-color: rgb(255, 255 , 255); }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 255, 255)\", 'message: Your body should have a white background.');", + "assert(code.match(/rgb\\s*\\(\\s*255\\s*,\\s*255\\s*,\\s*255\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of white. For example body { background-color: rgb(255, 255 , 255); }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa RGB para colorear de blanco los elementos", "descriptionEs": [ "El valor RGB para el negro, luce así:", @@ -4205,11 +3654,7 @@ "rgb(255, 255, 255)", "En lugar de utilizar seis dígitos hexadecimales, con rgb especificas el brillo de cada color con un número entre 0 y 255.", "Cambia el color de fondo del elemento body del valor RGB para el negro al valor rgb para el blanco: rgb(255, 255, 255)" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad89fee1348bd9aedf08724", @@ -4219,10 +3664,6 @@ "These values follow the pattern of RGB: the first number represents red, the second number represents green, and the third number represents blue.", "Change the body element's background color to the RGB value red: rgb(255, 0, 0)" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 0, 0)\", 'message: Your body should have a red background.');", - "assert(code.match(/rgb\\s*\\(\\s*255\\s*,\\s*0\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of red. For example body { background-color: rgb(255, 0, 0); }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 0, 0)\", 'message: Your body should have a red background.');", + "assert(code.match(/rgb\\s*\\(\\s*255\\s*,\\s*0\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of red. For example body { background-color: rgb(255, 0, 0); }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa RGB para colorear de rojo los elementos", "descriptionEs": [ "Al igual que con el código hexadecimal, puedes representar diferentes colores en RGB mediante el uso de combinaciones de diferentes valores.", "Estos valores siguen el patrón de RGB: el primer número representa rojo, el segundo número representa el verde, y el tercer número representa azul.", "Cambia el color de fondo del elemento body al rojo usando su valor RGB: rgb(255, 0, 0)" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad80fee1348bd9aedf08723", @@ -4255,10 +3690,6 @@ "description": [ "Now change the body element's background color to the rgb value green: rgb(0, 255, 0)" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(0, 255, 0)\", 'message: Your body element should have a green background.');", - "assert(code.match(/rgb\\s*\\(\\s*0\\s*,\\s*255\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of green. For example body { background-color: rgb(0, 255, 0); }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 255, 0)\", 'message: Your body element should have a green background.');", + "assert(code.match(/rgb\\s*\\(\\s*0\\s*,\\s*255\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of green. For example body { background-color: rgb(0, 255, 0); }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa RGB para colorear de verde los elementos", "descriptionEs": [ "Ahora cambia el color de fondo del elemento body a verde usando su valor RGB: rgb (0, 255, 0)" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad81fee1348bd9aedf08722", @@ -4289,10 +3714,6 @@ "description": [ "Change the body element's background color to the RGB value blue: rgb(0, 0, 255)" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 255)\", 'message: Your body element should have a blue background.');", - "assert(code.match(/rgb\\s*\\(\\s*0\\s*,\\s*0\\s*,\\s*255\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of blue. For example body { background-color: rgb(0, 0, 255); }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(0, 0, 255)\", 'message: Your body element should have a blue background.');", + "assert(code.match(/rgb\\s*\\(\\s*0\\s*,\\s*0\\s*,\\s*255\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of blue. For example body { background-color: rgb(0, 0, 255); }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa RGB para colorear de azul los elementos", "descriptionEs": [ "Cambia el color de fondo del elemento body a azul usando su valor RGB: rgb(0, 0, 255)" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] }, { "id": "bad82fee1348bd9aedf08721", @@ -4324,10 +3739,6 @@ "Just like with hex code, you can mix colors in RGB by using combinations of different values.", "Change the body element's background color to the RGB value orange: rgb(255, 165, 0)" ], - "tests": [ - "assert($(\"body\").css(\"background-color\") === \"rgb(255, 165, 0)\", 'message: Your body element should have an orange background.');", - "assert(code.match(/rgb\\s*\\(\\s*255\\s*,\\s*165\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of orange. For example body { background-color: rgb(255, 165, 0); }');" - ], "challengeSeed": [ "" ], + "tests": [ + "assert($(\"body\").css(\"background-color\") === \"rgb(255, 165, 0)\", 'message: Your body element should have an orange background.');", + "assert(code.match(/rgb\\s*\\(\\s*255\\s*,\\s*165\\s*,\\s*0\\s*\\)/ig), 'message: Use rgb to give your body element the background-color of orange. For example body { background-color: rgb(255, 165, 0); }');" + ], "type": "waypoint", "challengeType": 0, - "nameCn": "", - "descriptionCn": [], - "nameFr": "", - "descriptionFr": [], - "nameRu": "", - "descriptionRu": [], "nameEs": "Usa RGB para mezclar colores", "descriptionEs": [ "Al igual que con el código hexadecimal, puedes mezclar los colores en RGB mediante el uso de combinaciones de diferentes valores.", "Cambia el color de fondo del elemento body a anaranjado usando su valor RGB: rgb(255, 165, 0)" - ], - "namePt": "", - "descriptionPt": [], - "nameDe": "", - "descriptionDe": [] + ] } ] } From 66c04513cbed3a98d28c5b36d3b243b6b6520d85 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 8 Jan 2016 21:43:53 -0800 Subject: [PATCH 51/75] 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 c6f1468ff5e1beeb59367fd4c5a31a53e9816d6b Mon Sep 17 00:00:00 2001 From: Adegbuyi Ademola Date: Sat, 9 Jan 2016 08:26:11 +0100 Subject: [PATCH 52/75] fixed waypoint ins .hasOwnProperty([propname]) --- .../basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 2c42dae714..759cbc9a8b 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -3421,7 +3421,7 @@ "id": "567af2437cbaa8c51670a16c", "title": "Testing Objects for Properties", "description": [ - "Sometimes it is useful to check if the property of a given object exists or not. We can use the .hasOwnProperty([propname]) method of objects to determine if that object has the given property name. .hasOwnProperty() returns true or false if the property is found or not.", + "Sometimes it is useful to check if the property of a given object exists or not. We can use the .hasOwnProperty(propname) method of objects to determine if that object has the given property name. .hasOwnProperty() returns true or false if the property is found or not.", "Example", "
var myObj = {
top: \"hat\",
bottom: \"pants\"
};
myObj.hasOwnProperty(\"top\"); // true
myObj.hasOwnProperty(\"middle\"); // false
", "

Instructions

", From 7da7f7c47a6f3ba635361d1c5b45e6d77b9bd881 Mon Sep 17 00:00:00 2001 From: Robert Richey Date: Sat, 9 Jan 2016 14:13:57 -0700 Subject: [PATCH 53/75] Update news search field placeholder text Changed placeholder text from 'Search our links' to 'search term or @username'. Tested locally. --- server/views/stories/news-nav.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/views/stories/news-nav.jade b/server/views/stories/news-nav.jade index 2fdd18582f..c15c30b5d4 100644 --- a/server/views/stories/news-nav.jade +++ b/server/views/stories/news-nav.jade @@ -1,7 +1,7 @@ .row .col-xs-12.col-sm-9 .input-group - input#searchArea.big-text-field.field-responsive.form-control(type='text', placeholder='Search our links') + input#searchArea.big-text-field.field-responsive.form-control(type='text', placeholder='search term or @username') span.input-group-btn button#searchbutton.btn.btn-big.btn-primary.btn-responsive(type='button') Search .spacer From 4096f77c39b8028055f45c936abfce58ae109a76 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 17:00:49 -0800 Subject: [PATCH 54/75] 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 f72cd3c9868cc71ae0a559b41b15205540dd51ac Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 18:16:10 -0800 Subject: [PATCH 55/75] 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 e923513cea419e47785c68d39b6fa3c4cfd80319 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 18:38:15 -0800 Subject: [PATCH 56/75] 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 5bd52b0423a8af89f24a75af89497839fb11d7c6 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 18:40:07 -0800 Subject: [PATCH 57/75] 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 0316c317084550dbf9e554e886a5d25784c30899 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 19:59:17 -0800 Subject: [PATCH 58/75] 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 553f87822c6a972db3b6480983375cb09e296f7e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 20:08:01 -0800 Subject: [PATCH 59/75] 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 2277cde61bf7722d61f783ee70d38d7c274ee323 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:18:17 -0800 Subject: [PATCH 60/75] 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 a681949995e5680b93c71bdb1faea318c832f979 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:47:28 -0800 Subject: [PATCH 61/75] 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 bb2daa2a1f0c3f0a824f5c0d65d601d51516a8a7 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:48:48 -0800 Subject: [PATCH 62/75] 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 2f068261c65d89b93e74fbeea4d669b9f040c617 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 22:49:16 -0800 Subject: [PATCH 63/75] 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 2bef4fa840eb4c2c4c1d0ffc820c0e910235c7d9 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:05:53 -0800 Subject: [PATCH 64/75] 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 2553228d4c17d912e6b2d7713dfd9e367ea22f36 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:21:03 -0800 Subject: [PATCH 65/75] 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 8ce2cde619b4b4d66276933ce48f103e0c13e123 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:21:19 -0800 Subject: [PATCH 66/75] 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 86faa003accac3394caa376cc06ad49158fa3b10 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:23:04 -0800 Subject: [PATCH 67/75] 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 b6b2e9e02c14d31bffd8a1193279fe48e38932b8 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 9 Jan 2016 23:40:42 -0800 Subject: [PATCH 68/75] 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] } From 72488ed34d27369e0c9f5649c57bcf0403e36b2c Mon Sep 17 00:00:00 2001 From: Eric Leung Date: Sun, 10 Jan 2016 00:00:57 -0800 Subject: [PATCH 69/75] Add note on adding commas between JSON objects --- .../01-front-end-development-certification/basic-javascript.json | 1 + 1 file changed, 1 insertion(+) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 759cbc9a8b..20c9bd4a39 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -3467,6 +3467,7 @@ "Here is an example of a JSON object:", "
var ourMusic = [
{
\"artist\": \"Daft Punk\",
\"title\": \"Homework\",
\"release_year\": 1997,
\"formats\": [
\"CD\",
\"Cassette\",
\"LP\" ],
\"gold\": true
}
];
", "This is an array of objects and the object has various pieces of metadata about an album. It also has a nested formats array. Additional album records could be added to the top level array.", + "Note
You will need a comma in between objects in JSON objects with more than one object in the array.", "

Instructions

", "Add a new album to the myMusic JSON object. Add artist and title strings, release_year number, and a formats array of strings." ], From 7347f4d75ac4c71fbe440ff374758e84878cc54e Mon Sep 17 00:00:00 2001 From: Akira Laine Date: Sun, 10 Jan 2016 19:09:17 +1100 Subject: [PATCH 70/75] fixed semicolon typo --- .../basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 20c9bd4a39..52052d578a 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -757,7 +757,7 @@ "title": "Declare String Variables", "description": [ "Previously we have used the code", - "var myName = \"your name\"", + "var myName = \"your name\";", "\"your name\" is called a string literal. It is a string because it is a series of zero or more characters enclosed in single or double quotes.", "

Instructions

", "Create two new string variables: myFirstName and myLastName and assign them the values of your first and last name, respectively." From bb0f10ae66a95564efe0807d65f36427b27f842f Mon Sep 17 00:00:00 2001 From: Akira Laine Date: Sun, 10 Jan 2016 19:23:59 +1100 Subject: [PATCH 71/75] fixed spacing issue on waypoint: counting cards readded language stuff --- .../basic-javascript.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 52052d578a..29d8a708b2 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -870,10 +870,10 @@ "" ], "tail": [ - "(function(){", - "if (myStr !== undefined){", - "return 'myStr = '+ JSON.stringify(myStr);}", - "else{return null;}})();" + "(function(){", + "if (myStr !== undefined){", + "return 'myStr = '+ JSON.stringify(myStr);}", + "else{return null;}})();" ], "solutions": [ "var myStr = \"\\\\ \\t \\t \\r \\n\";" @@ -3011,9 +3011,8 @@ "In the casino game Blackjack, a player can gain an advantage over the house by keeping track of the relative number of high and low cards remaining in the deck. This is called Card Counting.", "Having more high cards remaining in the deck favors the player. Each card is assigned a value according to the table below. When the count is positive, the player should bet high. When the count is zero or negative, the player should bet low.", "
ValueCards
+12, 3, 4, 5, 6
07, 8, 9
-110, 'J', 'Q', 'K','A'
", - "You will write a card counting function. It will receive a card parameter and increment or decrement the global count variable according to the card's value (see table). The function will then return the current count and the string \"Bet\" if the count is positive, or \"Hold\" if the count is zero or negative.", - "Example Output", - "-3 Hold
5 Bet" + "You will write a card counting function. It will receive a card parameter and increment or decrement the global count variable according to the card's value (see table). The function will then return the current count and the string \"Bet\" if the count is positive, or \"Hold\" if the count is zero or negative.

", + "Example Output
-3 Hold
5 Bet
" ], "releasedOn": "January 1, 2016", "challengeSeed": [ From 8dc8ff82e0c87c581fe69ed4d54416fa427fadb7 Mon Sep 17 00:00:00 2001 From: Dmytro Yarmak Date: Sun, 10 Jan 2016 18:33:08 +0200 Subject: [PATCH 72/75] Update test for "Use Comments to Clarify Code" Allow blank lines before comment for "Waypoint: Use Comments to Clarify Code" challenge. --- .../01-front-end-development-certification/bootstrap.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/bootstrap.json b/seed/challenges/01-front-end-development-certification/bootstrap.json index d3566748b2..8fc4b9be3d 100644 --- a/seed/challenges/01-front-end-development-certification/bootstrap.json +++ b/seed/challenges/01-front-end-development-certification/bootstrap.json @@ -2256,7 +2256,7 @@ "Add a comment at the top of your HTML that says Only change code above this line." ], "tests": [ - "assert(code.match(/^.*this line)).*this line.*-->/gi), 'message: Your comment should have the text Only change code above this line.');", "assert(code.match(/-->.*\\n+.+/g), 'message: Be sure to close your comment with -->.');", "assert(code.match(//g).length, 'message: You should have the same number of comment openers and closers.');" From 61bd8fbc6976fc5df7ab1705f9b2ec563a2a5325 Mon Sep 17 00:00:00 2001 From: Eric Leung Date: Sun, 10 Jan 2016 00:33:54 -0800 Subject: [PATCH 73/75] Clarify output format for counting card Checkpoint --- .../basic-javascript.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 29d8a708b2..e030535717 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -3011,8 +3011,8 @@ "In the casino game Blackjack, a player can gain an advantage over the house by keeping track of the relative number of high and low cards remaining in the deck. This is called Card Counting.", "Having more high cards remaining in the deck favors the player. Each card is assigned a value according to the table below. When the count is positive, the player should bet high. When the count is zero or negative, the player should bet low.", "
ValueCards
+12, 3, 4, 5, 6
07, 8, 9
-110, 'J', 'Q', 'K','A'
", - "You will write a card counting function. It will receive a card parameter and increment or decrement the global count variable according to the card's value (see table). The function will then return the current count and the string \"Bet\" if the count is positive, or \"Hold\" if the count is zero or negative.

", - "Example Output
-3 Hold
5 Bet
" + "You will write a card counting function. It will receive a card parameter and increment or decrement the global count variable according to the card's value (see table). The function will then return a string with the current count and the string \"Bet\" if the count is positive, or \"Hold\" if the count is zero or negative. The current count and the player's decision (\"Bet\" or \"Hold\") should be separated by a single space.

", + "Example Output
\"-3 Hold\"
\"5 Bet\"
" ], "releasedOn": "January 1, 2016", "challengeSeed": [ From ee608f719a866fa47de3a3783a58f1d6ebe6bda0 Mon Sep 17 00:00:00 2001 From: Eric Leung Date: Sun, 10 Jan 2016 11:59:31 -0800 Subject: [PATCH 74/75] Add missing period in instructions --- .../basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index e030535717..2eaa6c56f5 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -107,7 +107,7 @@ "myNum = myVar;", "Assigns 5 to myVar and then resolves myVar to 5 again and assigns it to myNum.", "

Instructions

", - "Assign the value 7 to variable a", + "Assign the value 7 to variable a.", "Assign the contents of a to variable b." ], "releasedOn": "January 1, 2016", From 3053f99d1df0d84d50890207986ada22de32c7b4 Mon Sep 17 00:00:00 2001 From: patsul12 Date: Fri, 8 Jan 2016 14:28:48 -0800 Subject: [PATCH 75/75] added new logical order waypoint --- .../basic-javascript.json | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 2eaa6c56f5..ab5cd0097d 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -2652,6 +2652,47 @@ "type": "waypoint", "challengeType": 1 }, + { + "id": "5690307fddb111c6084545d7", + "title": "Logical Order in If Else Statements", + "description": [ + "Order is important in if, else if statements.", + "The loop is executed from top to bottom so you will want to be careful of what statement comes first.", + "Take these two functions as an example.", + "Heres the first:", + "
function foo(x) {
if (x < 1) {
return \"Less than one\";
} else if (num < 2) {
return \"Less than two\";
} else {
return \"Greater than or equal to two\";
}
}
", + "And the second just switches the order of the statements:", + "
function bar(x) {
if (x < 2) {
return \"Less than two\";
} else if (num < 1) {
return \"Less than one\";
} else {
return \"Greater than or equal to two\";
}
}
", + "While these two functions look nearly identical if we pass a number to both we get different outputs.", + "
foo(0) // \"Less than one\"
bar(0) // \"Less than two\"
", + "

Instructions

", + "Change the order of logic in the function so that it will return the correct statements in all cases." + ], + "challengeSeed": [ + "function myTest(val) {", + " if(val < 10) {", + " return \"Less than 10\";", + " } else if(val < 5) {", + " return \"Less than 5\";", + " } else {", + " return \"Greater than or equal to 10\";", + " }", + "}", + " ", + "// Change this value to test", + "myTest(7);" + ], + "solutions": [ + "function myTest(val) {\n if(val < 5) {\n return \"Less than 5\"; \n } else if (val < 10) {\n return \"Less than 10\";\n } else {\n return \"Greater than or equal to 10\";\n }\n}" + ], + "tests": [ + "assert(myTest(4) === \"Less than 5\", 'message: myTest(5) should return \"Less than 5\"');", + "assert(myTest(6) === \"Less than 10\", 'message: myTest(6) should return \"Less than 10\"');", + "assert(myTest(11) === \"Greater than or equal to 10\", 'message: myTest(11) should return \"Greater than or equal to 10\"');" + ], + "type": "waypoint", + "challengeType": 1 + }, { "id": "56533eb9ac21ba0edf2244dc", "title": "Chaining If Else Statements",