From 93029f4e35fb53c749c67633ec7f45d88766a794 Mon Sep 17 00:00:00 2001 From: Aniruddh Agarwal Date: Fri, 18 Sep 2015 10:37:32 +0800 Subject: [PATCH 01/42] Refactored intermediate bonfires expect to assert --- seed/challenges/intermediate-bonfires.json | 98 +++++++++++----------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/seed/challenges/intermediate-bonfires.json b/seed/challenges/intermediate-bonfires.json index 88c331fd7c..fb1173440e 100644 --- a/seed/challenges/intermediate-bonfires.json +++ b/seed/challenges/intermediate-bonfires.json @@ -19,11 +19,11 @@ "sumAll([1, 4]);" ], "tests": [ - "expect(sumAll([1, 4])).to.be.a('Number');", - "expect(sumAll([1, 4])).to.equal(10);", - "expect(sumAll([4, 1])).to.equal(10);", - "expect(sumAll([5, 10])).to.equal(45);", - "expect(sumAll([10, 5])).to.equal(45);" + "assert(typeof(sumAll([1, 4])) === \"number\", 'The result should be a number');", + "assert.deepEqual(sumAll([1, 4]), 10, 'sumAll([1, 4]) should return 10');", + "assert.deepEqual(sumAll([4, 1]), 10, 'sumAll([4, 1]) should return 10');", + "assert.deepEqual(sumAll([5, 10]), 45, 'sumAll([5, 10]) should return 45');", + "assert.deepEqual(sumAll([10, 5]), 45, 'sumAll([10, 5]) should return 45');" ], "MDNlinks": [ "Math.max()", @@ -61,7 +61,7 @@ "diff([1, 2, 3, 5], [1, 2, 3, 4, 5]);" ], "tests": [ - "expect(diff([1, 2, 3, 5], [1, 2, 3, 4, 5])).to.be.a('array');", + "assert(typeof(diff([1, 2, 3, 5], [1, 2, 3, 4, 5])) === \"object\", 'The result should be an array.');", "assert.deepEqual(diff(['diorite', 'andesite', 'grass', 'dirt', 'pink wool', 'dead shrub'], ['diorite', 'andesite', 'grass', 'dirt', 'dead shrub']), ['pink wool'], 'arrays with only one difference');", "assert.includeMembers(diff(['andesite', 'grass', 'dirt', 'pink wool', 'dead shrub'], ['diorite', 'andesite', 'grass', 'dirt', 'dead shrub']), ['diorite', 'pink wool'], 'arrays with more than one difference');", "assert.deepEqual(diff(['andesite', 'grass', 'dirt', 'dead shrub'], ['andesite', 'grass', 'dirt', 'dead shrub']), [], 'arrays with no difference');", @@ -93,11 +93,11 @@ "id": "a7f4d8f2483413a6ce226cac", "title": "Roman Numeral Converter", "tests": [ - "expect(convert(12)).to.equal(\"XII\");", - "expect(convert(5)).to.equal(\"V\");", - "expect(convert(9)).to.equal(\"IX\");", - "expect(convert(29)).to.equal(\"XXIX\");", - "expect(convert(16)).to.equal(\"XVI\");" + "assert.deepEqual(convert(12), \"XII\", 'convert(12) should return \"XII\"');", + "assert.deepEqual(convert(5), \"V\", 'convert(5) should return \"V\"');", + "assert.deepEqual(convert(9), \"IX\", 'convert(9) should return \"IX\"');", + "assert.deepEqual(convert(29), \"XXIX\", 'convert(29) should return \"XXIX\"');", + "assert.deepEqual(convert(16), \"XVI\", 'convert(16) should return \"XVI\"');" ], "difficulty": "2.02", "description": [ @@ -177,11 +177,11 @@ "id": "a0b5010f579e69b815e7c5d6", "title": "Search and Replace", "tests": [ - "expect(replace(\"Let us go to the store\", \"store\", \"mall\")).to.equal(\"Let us go to the mall\");", - "expect(replace(\"He is Sleeping on the couch\", \"Sleeping\", \"sitting\")).to.equal(\"He is Sitting on the couch\");", - "expect(replace(\"This has a spellngi error\", \"spellngi\", \"spelling\")).to.equal(\"This has a spelling error\");", - "expect(replace(\"His name is Tom\", \"Tom\", \"john\")).to.equal(\"His name is John\");", - "expect(replace(\"Let us get back to more Coding\", \"Coding\", \"bonfires\")).to.equal(\"Let us get back to more Bonfires\");" + "assert.deepEqual(replace(\"Let us go to the store\", \"store\", \"mall\"), \"Let us go to the mall\", 'replace(\"Let us go to the store\", \"store\", \"mall\") should return \"Let us go to the mall\"');", + "assert.deepEqual(replace(\"He is Sleeping on the couch\", \"Sleeping\", \"sitting\"), \"He is Sitting on the couch\", 'replace(\"He is Sleeping on the couch\", \"Sleeping\", \"sitting\") should return \"He is Sitting on the couch\"');", + "assert.deepEqual(replace(\"This has a spellngi error\", \"spellngi\", \"spelling\"), \"This has a spelling error\", 'replace(\"This has a spellngi error\", \"spellingi\", \"spelling\") should return \"This has a spelling error\"');", + "assert.deepEqual(replace(\"His name is Tom\", \"Tom\", \"john\"), \"His name is John\", 'replace(\"His name is Tom\", \"Tom\", \"john\") should return \"His name is John\"');", + "assert.deepEqual(replace(\"Let us get back to more Coding\", \"Coding\", \"bonfires\"), \"Let us get back to more Bonfires\", 'replace(\"Let us get back to more Coding\", \"Coding\", \"bonfires\") should return \"Let us get back to more Bonfires\"');" ], "difficulty": "2.035", "description": [ @@ -221,11 +221,11 @@ "id": "aa7697ea2477d1316795783b", "title": "Pig Latin", "tests": [ - "expect(translate(\"california\")).to.equal(\"aliforniacay\");", - "expect(translate(\"paragraphs\")).to.equal(\"aragraphspay\");", - "expect(translate(\"glove\")).to.equal(\"oveglay\");", - "expect(translate(\"algorithm\")).to.equal(\"algorithmway\");", - "expect(translate(\"eight\")).to.equal(\"eightway\");" + "assert.deepEqual(translate(\"california\"), \"aliforniacay\", 'translate(\"california\") should return \"aliforniacay\"');", + "assert.deepEqual(translate(\"paragraphs\"), \"aragraphspay\", 'translate(\"paragraphs\") should return \"aragraphspay\"');", + "assert.deepEqual(translate(\"glove\"), \"oveglay\", 'translate(\"glove\") should return \"oveglay\"');", + "assert.deepEqual(translate(\"algorithm\"), \"algorithmway\", 'translate(\"algorithm\") should return \"algorithmway\"');", + "assert.deepEqual(translate(\"eight\"), \"eightway\", 'translate(\"eight\") should return \"eightway\"');" ], "difficulty": "2.04", "description": [ @@ -319,10 +319,10 @@ "fearNotLetter('abce');" ], "tests": [ - "expect(fearNotLetter('abce')).to.equal('d');", - "expect(fearNotLetter('bcd')).to.be.undefined;", - "expect(fearNotLetter('abcdefghjklmno')).to.equal('i');", - "expect(fearNotLetter('yz')).to.be.undefined;" + "assert.deepEqual(fearNotLetter(\"abce\"), \"d\", 'fearNotLetter(\"abce\") should return d');", + "assert.deepEqual(fearNotLetter(\"abcdefghjklmno\"), \"i\", 'fearNotLetter(\"abcdefghjklmno\") should return i');", + "assert.isUndefined(fearNotLetter(\"bcd\"), 'fearNotLetter(\"bcd\") should return undefined');", + "assert.isUndefined(fearNotLetter(\"yz\"), 'fearNotLetter(\"yz\") should return undefined');" ], "MDNlinks": [ "String.charCodeAt()", @@ -525,12 +525,12 @@ "sumFibs(4);" ], "tests": [ - "expect(sumFibs(1)).to.be.a('number');", - "expect(sumFibs(1000)).to.equal(1785);", - "expect(sumFibs(4000000)).to.equal(4613732);", - "expect(sumFibs(4)).to.equal(5);", - "expect(sumFibs(75024)).to.equal(60696);", - "expect(sumFibs(75025)).to.equal(135721);" + "assert.deepEqual(typeof(sumFibs(1)), \"number\", \"The result should be a number\");", + "assert.deepEqual(sumFibs(1000), 1785, 'sumFibs(1000) should return 1785');", + "assert.deepEqual(sumFibs(4000000), 4613732, 'sumFibs(4000000) should return 4613732');", + "assert.deepEqual(sumFibs(4), 5, 'sumFibs(4) should return 5');", + "assert.deepEqual(sumFibs(75024), 60696, 'sumFibs(75024) should return 60696');", + "assert.deepEqual(sumFibs(75025), 135721, 'sumFibs(75025) should return 135721');" ], "MDNlinks": [ "Remainder" @@ -566,9 +566,9 @@ "sumPrimes(10);" ], "tests": [ - "expect(sumPrimes(10)).to.be.a('number');", - "expect(sumPrimes(10)).to.equal(17);", - "expect(sumPrimes(977)).to.equal(73156);" + "assert.deepEqual(typeof(sumPrimes(10)), \"number\", \"The result should be a number\");", + "assert.deepEqual(sumPrimes(10), 17, 'sumPrimes(10) should return 17');", + "assert.deepEqual(sumPrimes(977), 73156, 'sumPrimes(977) should return 73156');" ], "MDNlinks": [ "For Loops", @@ -606,10 +606,10 @@ "smallestCommons([1,5]);" ], "tests": [ - "expect(smallestCommons([1,5])).to.be.a('number');", - "expect(smallestCommons([1,5])).to.equal(60);", - "expect(smallestCommons([5,1])).to.equal(60);", - "expect(smallestCommons([1,13])).to.equal(360360);" + "assert.deepEqual(typeof(smallestCommons([1, 5])), \"number\", \"The result should be a number\");", + "assert.deepEqual(smallestCommons([1, 5]), 60, 'smallestCommons([1, 5]) should return 60');", + "assert.deepEqual(smallestCommons([5, 1]), 60, 'smallestCommons([5, 1]) should return 60');", + "assert.deepEqual(smallestCommons([1, 13]), 360360, 'smallestCommons([1, 13]) should return 360360');" ], "MDNlinks": [ "Smallest Common Multiple" @@ -680,10 +680,10 @@ "drop([1, 2, 3], function(n) {return n < 3; });" ], "tests": [ - "expect(drop([1, 2, 3, 4], function(n) {return n >= 3; })).to.eqls([3, 4]);", - "expect(drop([1, 2, 3], function(n) {return n > 0; })).to.eqls([1, 2, 3]);", - "expect(drop([1, 2, 3, 4], function(n) {return n > 5; })).to.eqls([]);", - "expect(drop([1, 2, 3, 7, 4], function(n) { return n > 5; })).to.eqls([7, 4]);" + "assert.deepEqual(drop([1, 2, 3, 4], function(n) {return n>= 3;}), [3, 4], 'drop([1, 2, 3, 4], function(n) {return n>= 3;}) should return [3, 4]');", + "assert.deepEqual(drop([1, 2, 3], function(n) {return n > 0; }), [1, 2, 3], 'drop([1, 2, 3], function(n) {return n > 0; }) should return [1, 2, 3]');", + "assert.deepEqual(drop([1, 2, 3, 4], function(n) {return n > 5;}), [], 'drop([1, 2, 3, 4], function(n) {return n > 5;}) should return []');", + "assert.deepEqual(drop([1, 2, 3, 7, 4], function(n) {return n > 3}), [7, 4], 'drop([1, 2, 3, 7, 4], function(n) {return n>= 3}) should return [7, 4]');" ], "MDNlinks": [ "Arguments object", @@ -757,8 +757,8 @@ "binaryAgent('01000001 01110010 01100101 01101110 00100111 01110100 00100000 01100010 01101111 01101110 01100110 01101001 01110010 01100101 01110011 00100000 01100110 01110101 01101110 00100001 00111111');" ], "tests": [ - "expect(binaryAgent('01000001 01110010 01100101 01101110 00100111 01110100 00100000 01100010 01101111 01101110 01100110 01101001 01110010 01100101 01110011 00100000 01100110 01110101 01101110 00100001 00111111')).to.equal(\"Aren't bonfires fun!?\");", - "expect(binaryAgent('01001001 00100000 01101100 01101111 01110110 01100101 00100000 01000110 01110010 01100101 01100101 01000011 01101111 01100100 01100101 01000011 01100001 01101101 01110000 00100001')).to.equal(\"I love FreeCodeCamp!\");" + "assert.deepEqual(binaryAgent('01000001 01110010 01100101 01101110 00100111 01110100 00100000 01100010 01101111 01101110 01100110 01101001 01110010 01100101 01110011 00100000 01100110 01110101 01101110 00100001 00111111'), \"Aren't bonfires fun!?\", \"binaryAgent() should return Aren't bonfires fun!?\");", + "assert.deepEqual(binaryAgent('01001001 00100000 01101100 01101111 01110110 01100101 00100000 01000110 01110010 01100101 01100101 01000011 01101111 01100100 01100101 01000011 01100001 01101101 01110000 00100001'), \"I love FreeCodeCamp!\", 'binaryAgent() should return \"I love FreeCodeCamp!\"');" ], "MDNlinks": [ "String.charCodeAt()", @@ -838,11 +838,11 @@ "add(2,3);" ], "tests": [ - "expect(add(2, 3)).to.equal(5);", - "expect(add(2)(3)).to.equal(5);", - "expect(add('http://bit.ly/IqT6zt')).to.be.undefined;", - "expect(add(2, '3')).to.be.undefined;", - "expect(add(2)([3])).to.be.undefined;" + "assert.deepEqual(add(2, 3), 5, 'add(2, 3) should return 5');", + "assert.deepEqual(add(2)(3), 5, 'add(2)(3) should return 5');", + "assert.isUndefined(add(\"http://bit.ly/IqT6zt\"), 'add(\"http://bit.ly/IqT6zt\") should return undefined');", + "assert.isUndefined(add(2, \"3\"), 'add(2, \"3\") should return undefined');", + "assert.isUndefined(add(2)([3]), 'add(2)([3]) should return undefined');" ], "MDNlinks": [ "Global Function Object", From 635d02db03d55a4eb6d00342489953c32d2016dd Mon Sep 17 00:00:00 2001 From: Arsen Melikyan Date: Sun, 20 Sep 2015 01:08:05 +0400 Subject: [PATCH 02/42] fixes a test in a bonfire --- seed/challenges/basic-bonfires.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/basic-bonfires.json b/seed/challenges/basic-bonfires.json index cbcdbd1b83..df356f9034 100644 --- a/seed/challenges/basic-bonfires.json +++ b/seed/challenges/basic-bonfires.json @@ -234,7 +234,7 @@ ], "tests": [ "assert(typeof(titleCase(\"I'm a little tea pot\")) === \"string\", 'titleCase() should return a string.');", - "assert(titleCase(\"I'm a little tea pot\") === \"I'm A Little Tea Pot\", '\"I'm a little tea pot\" should return \"I'm A Little Tea Pot\".');", + "assert(titleCase(\"I'm a little tea pot\") === \"I'm A Little Tea Pot\", '\"I'm a little tea pot\" should return \"I'm A Little Tea Pot\".');", "assert(titleCase(\"sHoRt AnD sToUt\") === \"Short And Stout\", '\"sHoRt AnD sToUt\" should return \"Short And Stout\".');", "assert(titleCase(\"HERE IS MY HANDLE HERE IS MY SPOUT\") === \"Here Is My Handle Here Is My Spout\", '\"HERE IS MY HANDLE HERE IS MY SPOUT\" should return \"Here Is My Handle Here Is My Spout\"');" ], From 6d8835ba5605fc476ddc8c920b9e9a8ed062b785 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 11 Sep 2015 10:58:24 -0700 Subject: [PATCH 03/42] return undefined if job is not found null values count as values when using default values so properties must be undefined when expecting default value to work --- common/app/routes/Jobs/flux/Actions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 4df2ae42c6..ce31070736 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -56,7 +56,10 @@ export default Actions({ debug('job services experienced an issue', err); return jobActions.setError({ err }); } - jobActions.setJobs({ currentJob: job }); + if (job) { + jobActions.setJobs({ currentJob: job }); + } + jobActions.setJobs({}); }); }); return jobActions; From e579cbd778433421b6eee03fb2c12c5aab1785c8 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 13 Sep 2015 18:12:22 -0700 Subject: [PATCH 04/42] update to react-router 1.0.0-rc1 --- client/index.js | 19 +++++++++++++------ common/app/app-stream.jsx | 12 ++++++------ common/app/routes/Jobs/components/Jobs.jsx | 6 +++--- package.json | 3 ++- server/boot/a-react.js | 18 +++++++++--------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/client/index.js b/client/index.js index 79c5c541c1..a7818eece5 100644 --- a/client/index.js +++ b/client/index.js @@ -4,7 +4,7 @@ import React from 'react'; import Fetchr from 'fetchr'; import debugFactory from 'debug'; import { Router } from 'react-router'; -import { history } from 'react-router/lib/BrowserHistory'; +import { createLocation, createHistory } from 'history'; import { hydrate } from 'thundercats'; import { Render } from 'thundercats-react'; @@ -18,21 +18,28 @@ const services = new Fetchr({ }); Rx.config.longStackSupport = !!debug.enabled; - +const history = createHistory(); +const appLocation = createLocation( + location.pathname + location.search +); // returns an observable -app$(history) +app$({ history, location: appLocation }) .flatMap( ({ AppCat }) => { + // instantiate the cat with service const appCat = AppCat(null, services); + // hydrate the stores return hydrate(appCat, catState) .map(() => appCat); }, - ({ initialState }, appCat) => ({ initialState, appCat }) + // not using nextLocation at the moment but will be used for + // redirects in the future + ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) - .flatMap(({ initialState, appCat }) => { + .flatMap(({ props, appCat }) => { return Render( appCat, - React.createElement(Router, initialState), + React.createElement(Router, props), DOMContianer ); }) diff --git a/common/app/app-stream.jsx b/common/app/app-stream.jsx index 25ae2a6300..82d5568c09 100644 --- a/common/app/app-stream.jsx +++ b/common/app/app-stream.jsx @@ -1,17 +1,17 @@ import Rx from 'rx'; -import { Router } from 'react-router'; +import { match } from 'react-router'; import App from './App.jsx'; import AppCat from './Cat'; import childRoutes from './routes'; -const router$ = Rx.Observable.fromNodeCallback(Router.run, Router); +const route$ = Rx.Observable.fromNodeCallback(match); const routes = Object.assign({ components: App }, childRoutes); -export default function app$(location) { - return router$(routes, location) - .map(([initialState, transistion]) => { - return { initialState, transistion, AppCat }; +export default function app$({ location, history }) { + return route$({ routes, location, history }) + .map(([nextLocation, props]) => { + return { nextLocation, props, AppCat }; }); } diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index a6bc6a9a9f..6c40cf6b0c 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,6 +1,6 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; -import { Navigation } from 'react-router'; +import { History } from 'react-router'; import { Button, Jumbotron, Row } from 'react-bootstrap'; import ListJobs from './List.jsx'; @@ -18,7 +18,7 @@ export default contain( jobActions: PropTypes.object, jobs: PropTypes.array }, - mixins: [Navigation], + mixins: [History], handleJobClick(id) { const { jobActions } = this.props; @@ -26,7 +26,7 @@ export default contain( return null; } jobActions.findJob(id); - this.transitionTo(`/jobs/${id}`); + this.history.pushState(null, `/jobs/${id}`); }, renderList(handleJobClick, jobs) { diff --git a/package.json b/package.json index 901b54ea3a..4b25530278 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "gulp-webpack": "^1.5.0", "helmet": "~0.9.0", "helmet-csp": "^0.2.3", + "history": "^1.9.0", "jade": "~1.8.0", "json-loader": "^0.5.2", "less": "~1.7.5", @@ -89,7 +90,7 @@ "react": "^0.13.3", "react-bootstrap": "~0.23.7", "react-motion": "~0.1.0", - "react-router": "https://github.com/BerkeleyTrue/react-router#freecodecamp", + "react-router": "^1.0.0-rc1", "react-vimeo": "^0.0.3", "request": "~2.53.0", "rev-del": "^1.0.5", diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 6c0cd04819..9b1f4926c9 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -1,7 +1,7 @@ import React from 'react'; -import Router from 'react-router'; +import { RoutingContext } from 'react-router'; import Fetchr from 'fetchr'; -import Location from 'react-router/lib/Location'; +import { createLocation } from 'history'; import debugFactory from 'debug'; import { app$ } from '../../common/app'; import { RenderToString } from 'thundercats-react'; @@ -30,25 +30,25 @@ export default function reactSubRouter(app) { function serveReactApp(req, res, next) { const services = new Fetchr({ req }); - const location = new Location(req.path, req.query); + const location = createLocation(req.path); // returns a router wrapped app - app$(location) + app$({ location }) // if react-router does not find a route send down the chain - .filter(function({ initialState }) { - if (!initialState) { + .filter(function({ props}) { + if (!props) { debug('react tried to find %s but got 404', location.pathname); return next(); } - return !!initialState; + return !!props; }) - .flatMap(function({ initialState, AppCat }) { + .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), - React.createElement(Router, initialState) + React.createElement(RoutingContext, props) ); }) // makes sure we only get one onNext and closes subscription From a34bcc2266cd33c6afbea1a224c1d6f999cba6a0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 13 Sep 2015 22:14:49 -0700 Subject: [PATCH 05/42] fix (hack) override history object with original --- client/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/index.js b/client/index.js index a7818eece5..44bdb0a82c 100644 --- a/client/index.js +++ b/client/index.js @@ -37,6 +37,7 @@ app$({ history, location: appLocation }) ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) .flatMap(({ props, appCat }) => { + props.history = history; return Render( appCat, React.createElement(Router, props), From fe144f7297deafbb88ea181a1db7590457d2f559 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 12:12:31 -0700 Subject: [PATCH 06/42] add highlighting to jobs --- common/app/routes/Jobs/components/List.jsx | 2 ++ common/app/routes/index.js | 12 ++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index ec8325a98b..2457bcb7f0 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -22,6 +22,7 @@ export default React.createClass({ id, company, position, + isHighlighted, description, logo, city, @@ -44,6 +45,7 @@ export default React.createClass({ ); return ( { - cb(null, [ - Jobs, - Hikes - ]); - }, 0); - } + childRoutes: [ + Jobs, + Hikes + ] }; From 523af406417025dbbb42d0346bb728a1d7fdd3fc Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 12:17:23 -0700 Subject: [PATCH 07/42] bump less remove old less middleware --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 4b25530278..16c58aae82 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,7 @@ "history": "^1.9.0", "jade": "~1.8.0", "json-loader": "^0.5.2", - "less": "~1.7.5", - "less-middleware": "~2.0.1", + "less": "~2.5.1", "lodash": "^3.9.3", "loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password", "loopback-boot": "2.8.2", From d8e8f3bb67982ffab614de6236695a6470b0eb3b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 13:06:27 -0700 Subject: [PATCH 08/42] add `create job` modal --- .../routes/Jobs/components/CreateJobModal.jsx | 33 +++++++++++++++++++ common/app/routes/Jobs/components/Jobs.jsx | 23 ++++++++++--- common/app/routes/Jobs/flux/Actions.js | 6 ++++ common/app/routes/Jobs/flux/Store.js | 12 +++++-- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 common/app/routes/Jobs/components/CreateJobModal.jsx diff --git a/common/app/routes/Jobs/components/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx new file mode 100644 index 0000000000..d3e6d34581 --- /dev/null +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -0,0 +1,33 @@ +import React, { PropTypes } from 'react'; +import { Button, Modal } from 'react-bootstrap'; + +export default React.createClass({ + displayName: 'CreateJobsModal', + propTypes: { + onHide: PropTypes.func, + showModal: PropTypes.bool + }, + + render() { + const { + showModal, + onHide + } = this.props; + + return ( + + +

Welcome to Free Code Camp's board

+

We post jobs specifically target to our junior developers.

+ +
+
+ ); + } +}); diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 6c40cf6b0c..a4d8354b3f 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -2,6 +2,8 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; import { History } from 'react-router'; import { Button, Jumbotron, Row } from 'react-bootstrap'; + +import CreateJobModal from './CreateJobModal.jsx'; import ListJobs from './List.jsx'; export default contain( @@ -13,12 +15,14 @@ export default contain( React.createClass({ displayName: 'Jobs', + mixins: [History], + propTypes: { children: PropTypes.element, jobActions: PropTypes.object, - jobs: PropTypes.array + jobs: PropTypes.array, + showModal: PropTypes.bool }, - mixins: [History], handleJobClick(id) { const { jobActions } = this.props; @@ -48,7 +52,12 @@ export default contain( }, render() { - const { children, jobs } = this.props; + const { + children, + jobs, + showModal, + jobActions + } = this.props; return (
@@ -62,7 +71,8 @@ export default contain(

@@ -70,7 +80,10 @@ export default contain( { this.renderChild(children, jobs) || this.renderList(this.handleJobClick, jobs) } - + +
); } diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index ce31070736..40c78025d0 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -31,6 +31,12 @@ export default Actions({ getJob: null, getJobs(params) { return { params }; + }, + openModal() { + return { showModal: true }; + }, + closeModal() { + return { showModal: false }; } }) .refs({ displayName: 'JobActions' }) diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index 2fdfa50207..abe3eb61cc 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -6,12 +6,20 @@ const { transformer } = Store; -export default Store() +export default Store({ showModal: false }) .refs({ displayName: 'JobsStore' }) .init(({ instance: jobsStore, args: [cat] }) => { - const { setJobs, findJob, setError } = cat.getActions('JobActions'); + const { + setJobs, + findJob, + setError, + openModal, + closeModal + } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); register(transformer(findJob)); register(setter(setError)); + register(setter(openModal)); + register(setter(closeModal)); }); From d3f2d603df7d2f36780624da2eb4f3e70e2730ca Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 17:31:24 -0700 Subject: [PATCH 09/42] fix nodemon should ignore seed files --- gulpfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/gulpfile.js b/gulpfile.js index afe9b421f5..fd5fdda38d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -49,6 +49,7 @@ var paths = { '!public/js/bundle*', 'node_modules/', 'client/', + 'seed', 'server/manifests/*.json', 'server/rev-manifest.json' ], From 41933a83604a4ebb0bcbca577b68e4cc378fe227 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 17:31:48 -0700 Subject: [PATCH 10/42] initial job form and job form nav --- .../routes/Jobs/components/CreateJobModal.jsx | 11 +++- common/app/routes/Jobs/components/NewJob.jsx | 56 +++++++++++++++++++ common/app/routes/Jobs/index.js | 4 ++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 common/app/routes/Jobs/components/NewJob.jsx diff --git a/common/app/routes/Jobs/components/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx index d3e6d34581..8a02400293 100644 --- a/common/app/routes/Jobs/components/CreateJobModal.jsx +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -1,13 +1,21 @@ import React, { PropTypes } from 'react'; +import { History } from 'react-router'; import { Button, Modal } from 'react-bootstrap'; export default React.createClass({ displayName: 'CreateJobsModal', + propTypes: { onHide: PropTypes.func, showModal: PropTypes.bool }, + mixins: [History], + + goToNewJob() { + this.history.pushState(null, '/jobs/new'); + }, + render() { const { showModal, @@ -23,7 +31,8 @@ export default React.createClass({

We post jobs specifically target to our junior developers.

diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx new file mode 100644 index 0000000000..a412ff6d7d --- /dev/null +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -0,0 +1,56 @@ +import React, { PropTypes } from 'react'; +import { contain } from 'thundercats-react'; +import { + Col, + Input, + Row, + Well +} from 'react-bootstrap'; + +export default contain({ + actions: 'jobActions', + store: 'jobsStore', + map({ form = {} }) { + return form; + } + }, + React.createClass({ + displayName: 'NewJob', + propTypes: { + jobActions: PropTypes.object + }, + render() { + return ( +
+ + + +

Create You Job Post

+
+ + + +
+
+ +
+
+ ); + } + }) +); diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index ac6b07f866..3564332d32 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -1,4 +1,5 @@ import Jobs from './components/Jobs.jsx'; +import NewJob from './components/NewJob.jsx'; import Show from './components/Show.jsx'; /* @@ -11,6 +12,9 @@ export default { childRoutes: [{ path: '/jobs', component: Jobs + }, { + path: 'jobs/new', + component: NewJob }, { path: 'jobs/:id', component: Show From 65572d65c8a234d388b059811dda32ca94a003db Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 21 Sep 2015 20:38:09 -0700 Subject: [PATCH 11/42] close modal before transition to job form --- common/app/routes/Jobs/components/CreateJobModal.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/app/routes/Jobs/components/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx index 8a02400293..446ed957d6 100644 --- a/common/app/routes/Jobs/components/CreateJobModal.jsx +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -12,7 +12,8 @@ export default React.createClass({ mixins: [History], - goToNewJob() { + goToNewJob(onHide) { + onHide(); this.history.pushState(null, '/jobs/new'); }, @@ -32,7 +33,7 @@ export default React.createClass({ From 10b3b8d75854187741fa008e1b5830377bd35bcf Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 21 Sep 2015 22:41:12 -0700 Subject: [PATCH 12/42] add validation to one input not sure this is the best approach --- common/app/routes/Jobs/components/NewJob.jsx | 55 +++++++++++++++++++- common/app/routes/Jobs/flux/Actions.js | 35 ++++++++++++- common/app/routes/Jobs/flux/Store.js | 7 ++- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index a412ff6d7d..61bf46530a 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -7,19 +7,57 @@ import { Well } from 'react-bootstrap'; +const defaults = { + 'string': { + value: '', + valid: false, + pristine: true + } +}; + +function defaultValue(type) { + return defaults[type]; +} + +function validatePosition(value) { + if (!value && typeof value !== 'string') { + return false; + } + return true; +} + export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { - return form; + const { + position = defaultValue('string'), + location = defaultValue('string'), + description = defaultValue('string') + } = form; + return { + position, + location, + description + }; } }, React.createClass({ displayName: 'NewJob', + propTypes: { - jobActions: PropTypes.object + jobActions: PropTypes.object, + position: PropTypes.object, + location: PropTypes.object, + description: PropTypes.object }, + render() { + const { + jobActions, + position + } = this.props; + return (
@@ -28,10 +66,23 @@ export default contain({

Create You Job Post

{ + jobActions.handleForm({ + name: 'position', + value, + validator: validatePosition + }); + }} placeholder='Position' type='text' + value={ position.value } wrapperClassName='col-xs-10' /> {} }) { + if (!name) { + // operation noop + return { replace: null }; + } + if (!validator(value)) { + return { + transform(oldState) { + const { oldForm } = oldState; + const newState = assign({}, oldState); + newState.form = assign( + {}, + oldForm, + { [name]: { value, valid: false, pristine: false }} + ); + return newState; + } + }; + } + return { + transform(oldState) { + const { oldForm } = oldState; + const newState = assign({}, oldState); + newState.form = assign( + {}, + oldForm, + { [name]: { value, valid: true, pristine: false }} + ); + return newState; + } + }; } }) .refs({ displayName: 'JobActions' }) diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index abe3eb61cc..a73235f1aa 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -14,12 +14,15 @@ export default Store({ showModal: false }) findJob, setError, openModal, - closeModal + closeModal, + handleForm } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); - register(transformer(findJob)); register(setter(setError)); register(setter(openModal)); register(setter(closeModal)); + + register(transformer(findJob)); + register(handleForm); }); From 5258145ef601d607ef2f2a90e9f04b20a7b2bc8d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 13:56:55 -0700 Subject: [PATCH 13/42] add validation to all current inputs validation right now is simply validating that the value is indeed a string --- common/app/routes/Jobs/components/NewJob.jsx | 41 +++++++++++++++++--- common/app/routes/Jobs/flux/Actions.js | 8 ++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 61bf46530a..b1b1ff30c3 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -19,7 +19,7 @@ function defaultValue(type) { return defaults[type]; } -function validatePosition(value) { +function validateString(value) { if (!value && typeof value !== 'string') { return false; } @@ -32,12 +32,12 @@ export default contain({ map({ form = {} }) { const { position = defaultValue('string'), - location = defaultValue('string'), + locale = defaultValue('string'), description = defaultValue('string') } = form; return { position, - location, + locale, description }; } @@ -48,14 +48,16 @@ export default contain({ propTypes: { jobActions: PropTypes.object, position: PropTypes.object, - location: PropTypes.object, + locale: PropTypes.object, description: PropTypes.object }, render() { const { jobActions, - position + position, + locale, + description } = this.props; return ( @@ -77,7 +79,7 @@ export default contain({ jobActions.handleForm({ name: 'position', value, - validator: validatePosition + validator: validateString }); }} placeholder='Position' @@ -85,16 +87,43 @@ export default contain({ value={ position.value } wrapperClassName='col-xs-10' /> { + jobActions.handleForm({ + name: 'locale', + value, + validator: validateString + }); + }} placeholder='Location' type='text' + value={ locale.value } wrapperClassName='col-xs-10' /> { + jobActions.handleForm({ + name: 'description', + value, + validator: validateString + }); + }} placeholder='Description' + rows='10' type='textarea' + value={ description.value } wrapperClassName='col-xs-10' /> diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 664d04caf3..ccaab9c755 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -47,11 +47,11 @@ export default Actions({ if (!validator(value)) { return { transform(oldState) { - const { oldForm } = oldState; + const { form } = oldState; const newState = assign({}, oldState); newState.form = assign( {}, - oldForm, + form, { [name]: { value, valid: false, pristine: false }} ); return newState; @@ -60,11 +60,11 @@ export default Actions({ } return { transform(oldState) { - const { oldForm } = oldState; + const { form } = oldState; const newState = assign({}, oldState); newState.form = assign( {}, - oldForm, + form, { [name]: { value, valid: true, pristine: false }} ); return newState; From 98af05256a7c838461394cbd65c410a0b3cc176e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 16:10:12 -0700 Subject: [PATCH 14/42] switch to validator add email field --- common/app/routes/Jobs/components/NewJob.jsx | 64 +++++++++++--------- common/app/routes/Jobs/flux/Actions.js | 14 ++++- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index b1b1ff30c3..83f773e05d 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -6,6 +6,10 @@ import { Row, Well } from 'react-bootstrap'; +import { + isAscii, + isEmail +} from 'validator'; const defaults = { 'string': { @@ -19,13 +23,6 @@ function defaultValue(type) { return defaults[type]; } -function validateString(value) { - if (!value && typeof value !== 'string') { - return false; - } - return true; -} - export default contain({ actions: 'jobActions', store: 'jobsStore', @@ -33,12 +30,14 @@ export default contain({ const { position = defaultValue('string'), locale = defaultValue('string'), - description = defaultValue('string') + description = defaultValue('string'), + email = defaultValue('string') } = form; return { position, locale, - description + description, + email }; } }, @@ -49,7 +48,8 @@ export default contain({ jobActions: PropTypes.object, position: PropTypes.object, locale: PropTypes.object, - description: PropTypes.object + description: PropTypes.object, + email: PropTypes.object }, render() { @@ -57,7 +57,8 @@ export default contain({ jobActions, position, locale, - description + description, + email } = this.props; return ( @@ -65,21 +66,17 @@ export default contain({ -

Create You Job Post

+

Create Your Job Post

{ jobActions.handleForm({ name: 'position', value, - validator: validateString + validator: isAscii }); }} placeholder='Position' @@ -87,18 +84,14 @@ export default contain({ value={ position.value } wrapperClassName='col-xs-10' /> { jobActions.handleForm({ name: 'locale', value, - validator: validateString + validator: isAscii }); }} placeholder='Location' @@ -106,18 +99,14 @@ export default contain({ value={ locale.value } wrapperClassName='col-xs-10' /> { jobActions.handleForm({ name: 'description', value, - validator: validateString + validator: isAscii }); }} placeholder='Description' @@ -125,6 +114,21 @@ export default contain({ type='textarea' value={ description.value } wrapperClassName='col-xs-10' /> + { + jobActions.handleForm({ + name: 'email', + value, + validator: isEmail + }); + }} + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName='col-xs-10' />
diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index ccaab9c755..5b284e4b8c 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -52,7 +52,12 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { value, valid: false, pristine: false }} + { [name]: { + value, + valid: false, + pristine: false, + bsStyle: value ? 'error' : null + }} ); return newState; } @@ -65,7 +70,12 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { value, valid: true, pristine: false }} + { [name]: { + value, + valid: true, + pristine: false, + bsStyle: value ? 'success' : null + }} ); return newState; } From 70b823ca63b7a3d377af7c0dca9800177020b5c4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 17:19:14 -0700 Subject: [PATCH 15/42] add phone number input change validation function scheme update validator --- common/app/routes/Jobs/components/NewJob.jsx | 168 +++++++++++-------- common/models/job.json | 1 + package.json | 2 +- 3 files changed, 103 insertions(+), 68 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 83f773e05d..ae1e5eab51 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -8,7 +8,8 @@ import { } from 'react-bootstrap'; import { isAscii, - isEmail + isEmail, + isMobilePhone } from 'validator'; const defaults = { @@ -31,13 +32,15 @@ export default contain({ position = defaultValue('string'), locale = defaultValue('string'), description = defaultValue('string'), - email = defaultValue('string') + email = defaultValue('string'), + phone = defaultValue('string') } = form; return { position, locale, description, - email + email, + phone }; } }, @@ -49,17 +52,25 @@ export default contain({ position: PropTypes.object, locale: PropTypes.object, description: PropTypes.object, - email: PropTypes.object + email: PropTypes.object, + phone: PropTypes.object + }, + + handleChange(name, validator, { target: { value } }) { + const { jobActions: { handleForm } } = this.props; + handleForm({ name, value, validator }); }, render() { const { - jobActions, position, locale, description, - email + email, + phone } = this.props; + const labelClass = 'col-sm-offset-1 col-sm-2'; + const inputClass = 'col-sm-6'; return (
@@ -68,67 +79,90 @@ export default contain({

Create Your Job Post

- { - jobActions.handleForm({ - name: 'position', - value, - validator: isAscii - }); - }} - placeholder='Position' - type='text' - value={ position.value } - wrapperClassName='col-xs-10' /> - { - jobActions.handleForm({ - name: 'locale', - value, - validator: isAscii - }); - }} - placeholder='Location' - type='text' - value={ locale.value } - wrapperClassName='col-xs-10' /> - { - jobActions.handleForm({ - name: 'description', - value, - validator: isAscii - }); - }} - placeholder='Description' - rows='10' - type='textarea' - value={ description.value } - wrapperClassName='col-xs-10' /> - { - jobActions.handleForm({ - name: 'email', - value, - validator: isEmail - }); - }} - placeholder='Email' - type='email' - value={ email.value } - wrapperClassName='col-xs-10' /> + +
+

Job Information

+
+ { + this.handleChange( + 'position', + isAscii, + e + ); + }} + placeholder='Position' + type='text' + value={ position.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'locale', + isAscii, + e, + ); + }} + placeholder='Location' + type='text' + value={ locale.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'description', + isAscii, + e + ); + }} + placeholder='Description' + rows='10' + type='textarea' + value={ description.value } + wrapperClassName={ inputClass } /> +
+

Company Information

+
+ { + this.handleChange( + 'email', + isEmail, + e + ); + }} + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'phone', + (data) => isMobilePhone(data, 'en-US'), + e + ); + }} + placeholder='555-123-1234' + type='tel' + value={ phone.value } + wrapperClassName={ inputClass } /> +
diff --git a/common/models/job.json b/common/models/job.json index a3392fee82..197f0619bf 100644 --- a/common/models/job.json +++ b/common/models/job.json @@ -1,6 +1,7 @@ { "name": "job", "base": "PersistedModel", + "strict": true, "idInjection": true, "trackChanges": false, "properties": { diff --git a/package.json b/package.json index 16c58aae82..df579eb2bc 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "thundercats-react": "^0.1.0", "twit": "~1.1.20", "uglify-js": "~2.4.15", - "validator": "~3.22.1", + "validator": "^3.22.1", "webpack": "^1.9.12", "yui": "~3.18.1" }, From 2ee22340503511d6671ac5c5c837f13f768a1e1f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 17:26:53 -0700 Subject: [PATCH 16/42] add company URL --- common/app/routes/Jobs/components/NewJob.jsx | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index ae1e5eab51..fd5e2aafa9 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -9,7 +9,8 @@ import { import { isAscii, isEmail, - isMobilePhone + isMobilePhone, + isURL } from 'validator'; const defaults = { @@ -33,14 +34,16 @@ export default contain({ locale = defaultValue('string'), description = defaultValue('string'), email = defaultValue('string'), - phone = defaultValue('string') + phone = defaultValue('string'), + url = defaultValue('string') } = form; return { position, locale, description, email, - phone + phone, + url }; } }, @@ -53,7 +56,8 @@ export default contain({ locale: PropTypes.object, description: PropTypes.object, email: PropTypes.object, - phone: PropTypes.object + phone: PropTypes.object, + url: PropTypes.object }, handleChange(name, validator, { target: { value } }) { @@ -67,7 +71,8 @@ export default contain({ locale, description, email, - phone + phone, + url } = this.props; const labelClass = 'col-sm-offset-1 col-sm-2'; const inputClass = 'col-sm-6'; @@ -162,6 +167,21 @@ export default contain({ type='tel' value={ phone.value } wrapperClassName={ inputClass } /> + { + this.handleChange( + 'url', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> From 01a40500591539c75aaa5070354725efee5e0809 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 18:25:09 -0700 Subject: [PATCH 17/42] Add higlight, company name --- common/app/routes/Jobs/components/NewJob.jsx | 86 ++++++++++++++++---- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index fd5e2aafa9..f76113959c 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -18,24 +18,26 @@ const defaults = { value: '', valid: false, pristine: true + }, + bool: { + value: false } }; -function defaultValue(type) { - return defaults[type]; -} - export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { const { - position = defaultValue('string'), - locale = defaultValue('string'), - description = defaultValue('string'), - email = defaultValue('string'), - phone = defaultValue('string'), - url = defaultValue('string') + position = defaults['string'], + locale = defaults['string'], + description = defaults['string'], + email = defaults['string'], + phone = defaults['string'], + url = defaults['string'], + logo = defaults['string'], + name = defaults['string'], + highlight = defaults['bool'] } = form; return { position, @@ -43,7 +45,10 @@ export default contain({ description, email, phone, - url + url, + logo, + name, + highlight }; } }, @@ -57,7 +62,10 @@ export default contain({ description: PropTypes.object, email: PropTypes.object, phone: PropTypes.object, - url: PropTypes.object + url: PropTypes.object, + logo: PropTypes.object, + name: PropTypes.object, + highlight: PropTypes.object }, handleChange(name, validator, { target: { value } }) { @@ -72,7 +80,10 @@ export default contain({ description, email, phone, - url + url, + logo, + name, + highlight } = this.props; const labelClass = 'col-sm-offset-1 col-sm-2'; const inputClass = 'col-sm-6'; @@ -134,9 +145,25 @@ export default contain({ type='textarea' value={ description.value } wrapperClassName={ inputClass } /> +

Company Information

+ { + this.handleChange( + 'name', + isAscii, + e, + ); + }} + placeholder='Foo, INC' + type='text' + value={ name.value } + wrapperClassName={ inputClass } /> { this.handleChange( @@ -182,6 +209,37 @@ export default contain({ type='url' value={ url.value } wrapperClassName={ inputClass } /> + { + this.handleChange( + 'logo', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+

Make it stand out

+
+ { + this.handleChange( + 'highlight', + () => { return true; }, + e + ); + }} + type='checkbox' + value={ highlight.value } /> From 326ed993463f0c7eec9342b8a57e773c1d20072f Mon Sep 17 00:00:00 2001 From: Aniruddh Agarwal Date: Wed, 23 Sep 2015 16:27:42 +0800 Subject: [PATCH 18/42] Renamed zipline for consistency Renamed the 'Wikipedia Viewer' ziplines to 'Build a Wikipedia Viewer' to make it consistent with the other zipline names. --- seed/challenges/intermediate-ziplines.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/intermediate-ziplines.json b/seed/challenges/intermediate-ziplines.json index ae92f3879d..c119db33af 100644 --- a/seed/challenges/intermediate-ziplines.json +++ b/seed/challenges/intermediate-ziplines.json @@ -37,7 +37,7 @@ }, { "id": "bd7158d8c442eddfaeb5bd19", - "title": "Wikipedia Viewer", + "title": "Build a Wikipedia Viewer", "difficulty": 1.03, "challengeSeed": ["126415131"], "description": [ From d8a6373b1ec46545d7f2435be64ecb4aeddf525a Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 23 Sep 2015 13:31:27 -0700 Subject: [PATCH 19/42] add submit button --- common/app/routes/Jobs/components/NewJob.jsx | 301 ++++++++++--------- 1 file changed, 157 insertions(+), 144 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index f76113959c..41d20db840 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; import { contain } from 'thundercats-react'; import { + Button, Col, Input, Row, @@ -95,151 +96,163 @@ export default contain({

Create Your Job Post

+ +
+

Job Information

+
+ { + this.handleChange( + 'position', + isAscii, + e + ); + }} + placeholder='Position' + type='text' + value={ position.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'locale', + isAscii, + e, + ); + }} + placeholder='Location' + type='text' + value={ locale.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'description', + isAscii, + e + ); + }} + placeholder='Description' + rows='10' + type='textarea' + value={ description.value } + wrapperClassName={ inputClass } /> + +
+

Company Information

+
+ { + this.handleChange( + 'name', + isAscii, + e, + ); + }} + placeholder='Foo, INC' + type='text' + value={ name.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'email', + isEmail, + e + ); + }} + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'phone', + (data) => isMobilePhone(data, 'en-US'), + e + ); + }} + placeholder='555-123-1234' + type='tel' + value={ phone.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'url', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'logo', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+

Make it stand out

+
+ { + this.handleChange( + 'highlight', + () => { return true; }, + e + ); + }} + type='checkbox' + value={ highlight.value } /> +
-
-

Job Information

-
- { - this.handleChange( - 'position', - isAscii, - e - ); - }} - placeholder='Position' - type='text' - value={ position.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'locale', - isAscii, - e, - ); - }} - placeholder='Location' - type='text' - value={ locale.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'description', - isAscii, - e - ); - }} - placeholder='Description' - rows='10' - type='textarea' - value={ description.value } - wrapperClassName={ inputClass } /> - -
-

Company Information

-
- { - this.handleChange( - 'name', - isAscii, - e, - ); - }} - placeholder='Foo, INC' - type='text' - value={ name.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'email', - isEmail, - e - ); - }} - placeholder='Email' - type='email' - value={ email.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'phone', - (data) => isMobilePhone(data, 'en-US'), - e - ); - }} - placeholder='555-123-1234' - type='tel' - value={ phone.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'url', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} - placeholder='http://freecatphotoapp.com' - type='url' - value={ url.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'logo', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} - placeholder='http://freecatphotoapp.com/logo.png' - type='url' - value={ logo.value } - wrapperClassName={ inputClass } /> - -
-

Make it stand out

-
- { - this.handleChange( - 'highlight', - () => { return true; }, - e - ); - }} - type='checkbox' - value={ highlight.value } /> + + +
From bf95314830d01d4e2bf4509acedb8b1d466fa9a4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 23 Sep 2015 15:05:27 -0700 Subject: [PATCH 20/42] fix prevent redirect to /news/hot --- server/middlewares/add-return-to.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/middlewares/add-return-to.js b/server/middlewares/add-return-to.js index 59163d42da..6771c38481 100644 --- a/server/middlewares/add-return-to.js +++ b/server/middlewares/add-return-to.js @@ -26,13 +26,12 @@ export default function addReturnToUrl() { return function(req, res, next) { // Remember original destination before login. var path = req.path.split('/')[1]; - var subPath = req.path.split('/')[2]; if ( req.method !== 'GET' || pathsOfNoReturnRegex.test(path) || !whiteListRegex.test(path) || - (/news/i).test(path) && (/hot/i).test(subPath) + (/news/i).test(path) && (/hot/i).test(req.path) ) { return next(); } From de976c18babed60fe49adb697159876f080fb36e Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Wed, 23 Sep 2015 15:20:45 -0700 Subject: [PATCH 21/42] remove email address from stories --- common/models/story.json | 4 ---- server/boot/story.js | 7 ++----- server/views/stories/show.jade | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/common/models/story.json b/common/models/story.json index 5d8e56dfb2..d338199a48 100644 --- a/common/models/story.json +++ b/common/models/story.json @@ -34,10 +34,6 @@ "description": { "type": "string" }, - "originalStoryAuthorEmail": { - "type": "string", - "default": "" - }, "rank": { "type": "number", "default": 0 diff --git a/server/boot/story.js b/server/boot/story.js index 02b92e93b3..d46b79f079 100755 --- a/server/boot/story.js +++ b/server/boot/story.js @@ -170,7 +170,6 @@ module.exports = function(app) { title: story.headline, link: story.link, originalStoryLink: dashedName, - originalStoryAuthorEmail: story.author.email || '', author: story.author, rank: story.upVotes.length, upVotes: story.upVotes, @@ -373,13 +372,11 @@ module.exports = function(app) { author: { picture: req.user.picture, userId: req.user.id, - username: req.user.username, - email: req.user.email + username: req.user.username }, image: data.image, storyLink: storyLink, - metaDescription: data.storyMetaDescription, - originalStoryAuthorEmail: req.user.email + metaDescription: data.storyMetaDescription }); return saveInstance(newStory); }); diff --git a/server/views/stories/show.jade b/server/views/stories/show.jade index 20c6346e01..cced0f41f5 100644 --- a/server/views/stories/show.jade +++ b/server/views/stories/show.jade @@ -1,7 +1,6 @@ script. var storyId = !{JSON.stringify(id)}; var originalStoryLink = !{JSON.stringify(originalStoryLink)}; - var originalStoryAuthorEmail = !{JSON.stringify(originalStoryAuthorEmail)}; var upVotes = !{JSON.stringify(upVotes)}; var image = !{JSON.stringify(image)}; var hasUserVoted = !{JSON.stringify(hasUserVoted)}; From e9108531860d66aa9ea52aeedbed380ea1c5104b Mon Sep 17 00:00:00 2001 From: Eric Leung Date: Tue, 22 Sep 2015 00:22:02 -0700 Subject: [PATCH 22/42] add note about how jQuery is zero-indexed for clarification --- seed/challenges/jquery.json | 1 + 1 file changed, 1 insertion(+) diff --git a/seed/challenges/jquery.json b/seed/challenges/jquery.json index 3a7b988816..95214991e4 100644 --- a/seed/challenges/jquery.json +++ b/seed/challenges/jquery.json @@ -791,6 +791,7 @@ "description": [ "You can also target all the even-numbered elements.", "Here's how you would target all the odd-numbered elements with class target and give them classes: $(\".target:odd\").addClass(\"animated shake\");", + "Note that jQuery is zero-indexed, meaning that, counter-intuitively, :odd selects the second element, fourth element, and so on.", "Try selecting all the even-numbered elements - that is, what your browser will consider even-numbered elements - and giving them the classes of animated and shake." ], "tests": [ From 8148c1a19cedbd1cae09f11d799576f1e099be6f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 24 Sep 2015 20:28:04 -0700 Subject: [PATCH 23/42] save form to localStorage --- common/app/routes/Jobs/components/NewJob.jsx | 121 +++++++++++++++---- common/app/routes/Jobs/flux/Actions.js | 52 ++++++-- common/app/routes/Jobs/flux/Store.js | 4 +- common/app/routes/Jobs/utils.js | 22 ++++ common/models/job.json | 5 +- package.json | 2 + 6 files changed, 169 insertions(+), 37 deletions(-) create mode 100644 common/app/routes/Jobs/utils.js diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 41d20db840..b160671c4b 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,5 +1,13 @@ import React, { PropTypes } from 'react'; import { contain } from 'thundercats-react'; +import debugFactory from 'debug'; +import { getDefaults } from '../utils'; + +import { + inHTMLData, + uriInSingleQuotedAttr +} from 'xss-filters'; + import { Button, Col, @@ -7,6 +15,7 @@ import { Row, Well } from 'react-bootstrap'; + import { isAscii, isEmail, @@ -14,31 +23,34 @@ import { isURL } from 'validator'; -const defaults = { - 'string': { - value: '', - valid: false, - pristine: true - }, - bool: { - value: false - } -}; +const debug = debugFactory('freecc:jobs:newForm'); + +const checkValidity = [ + 'position', + 'locale', + 'description', + 'email', + 'phone', + 'url', + 'logo', + 'name', + 'highlight' +]; export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { const { - position = defaults['string'], - locale = defaults['string'], - description = defaults['string'], - email = defaults['string'], - phone = defaults['string'], - url = defaults['string'], - logo = defaults['string'], - name = defaults['string'], - highlight = defaults['bool'] + position = getDefaults('string'), + locale = getDefaults('string'), + description = getDefaults('string'), + email = getDefaults('string'), + phone = getDefaults('string'), + url = getDefaults('string'), + logo = getDefaults('string'), + name = getDefaults('string'), + highlight = getDefaults('bool') } = form; return { position, @@ -51,6 +63,9 @@ export default contain({ name, highlight }; + }, + subscribeOnWillMount() { + return typeof window !== 'undefined'; } }, React.createClass({ @@ -69,6 +84,63 @@ export default contain({ highlight: PropTypes.object }, + handleSubmit(e) { + e.preventDefault(); + let valid = true; + checkValidity.forEach((prop) => { + // if value exist, check if it is valid + if (this.props[prop].value) { + valid = valid && !!this.props[prop].valid; + } + }); + + if (!valid) { + debug('form not valid'); + return; + } + + const { + position, + locale, + description, + email, + phone, + url, + logo, + name, + highlight, + jobActions + } = this.props; + + // sanitize user output + const jobValues = { + position: inHTMLData(position.value), + location: inHTMLData(locale.value), + description: inHTMLData(description.value), + email: inHTMLData(email.value), + phone: inHTMLData(phone.value), + url: uriInSingleQuotedAttr(url.value), + logo: uriInSingleQuotedAttr(logo.value), + name: inHTMLData(name.value), + highlight: !!highlight.value + }; + + const job = Object.keys(jobValues).reduce((accu, prop) => { + if (jobValues[prop]) { + accu[prop] = jobValues[prop]; + } + return accu; + }, {}); + + debug('job sanitized', job); + jobActions.saveForm(job); + }, + + componentDidMount() { + const { jobActions } = this.props; + jobActions.getSavedForm(); + }, + handleChange(name, validator, { target: { value } }) { const { jobActions: { handleForm } } = this.props; handleForm({ name, value, validator }); @@ -95,7 +167,9 @@ export default contain({

Create Your Job Post

-
+

Job Information

@@ -151,7 +225,7 @@ export default contain({

Company Information

{ @@ -248,8 +322,9 @@ export default contain({ lgOffset={ 3 }> diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 5b284e4b8c..771c520013 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,4 +1,6 @@ import { Actions } from 'thundercats'; +import store from 'store'; +import { getDefaults } from '../utils'; import debugFactory from 'debug'; const debug = debugFactory('freecc:jobs:actions'); @@ -52,12 +54,14 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { - value, - valid: false, - pristine: false, - bsStyle: value ? 'error' : null - }} + { + [name]: { + value, + valid: false, + pristine: false, + bsStyle: value ? 'error' : null + } + } ); return newState; } @@ -70,16 +74,31 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { - value, - valid: true, - pristine: false, - bsStyle: value ? 'success' : null - }} + { + [name]: { + value, + valid: true, + pristine: false, + bsStyle: value ? 'success' : null + } + } ); return newState; } }; + }, + saveForm: null, + getSavedForm: null, + setForm(job) { + const form = Object.keys(job).reduce((accu, prop) => { + console.log('form', accu); + return Object.assign( + accu, + { [prop]: getDefaults(typeof prop, job[prop]) } + ); + }, {}); + + return { form }; } }) .refs({ displayName: 'JobActions' }) @@ -111,5 +130,14 @@ export default Actions({ jobActions.setJobs({}); }); }); + + jobActions.saveForm.subscribe((form) => { + store.set('newJob', form); + }); + + jobActions.getSavedForm.subscribe(() => { + const job = store.get('newJob'); + jobActions.setForm(job); + }); return jobActions; }); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index a73235f1aa..b2f5132013 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -15,13 +15,15 @@ export default Store({ showModal: false }) setError, openModal, closeModal, - handleForm + handleForm, + setForm } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); register(setter(setError)); register(setter(openModal)); register(setter(closeModal)); + register(setter(setForm)); register(transformer(findJob)); register(handleForm); diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js new file mode 100644 index 0000000000..3a60c373a8 --- /dev/null +++ b/common/app/routes/Jobs/utils.js @@ -0,0 +1,22 @@ +const defaults = { + 'string': { + value: '', + valid: false, + pristine: true, + type: 'string' + }, + bool: { + value: false, + type: 'boolean' + } +}; + +export function getDefaults(type, value) { + if (!type) { + return defaults['string']; + } + if (value) { + return Object.assign({}, defaults[type], { value }); + } + return defaults[type]; +} diff --git a/common/models/job.json b/common/models/job.json index 197f0619bf..f77fc6defa 100644 --- a/common/models/job.json +++ b/common/models/job.json @@ -30,6 +30,9 @@ "state": { "type": "string" }, + "url": { + "type": "string" + }, "country": { "type": "string" }, @@ -39,7 +42,7 @@ "description": { "type": "string" }, - "isApproverd": { + "isApproved": { "type": "boolean" }, "isHighlighted": { diff --git a/package.json b/package.json index df579eb2bc..f16e4bb9db 100644 --- a/package.json +++ b/package.json @@ -97,12 +97,14 @@ "sanitize-html": "~1.6.1", "sort-keys": "^1.1.1", "source-map-support": "^0.3.2", + "store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server", "thundercats": "^2.1.0", "thundercats-react": "^0.1.0", "twit": "~1.1.20", "uglify-js": "~2.4.15", "validator": "^3.22.1", "webpack": "^1.9.12", + "xss-filters": "^1.2.6", "yui": "~3.18.1" }, "devDependencies": { From c63a983fb93a5352b82eedeaf5e9c4e51379d064 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 25 Sep 2015 00:04:38 -0700 Subject: [PATCH 24/42] filter output from localStorage --- common/app/routes/Jobs/flux/Actions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 771c520013..15229e039d 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -137,7 +137,9 @@ export default Actions({ jobActions.getSavedForm.subscribe(() => { const job = store.get('newJob'); - jobActions.setForm(job); + if (job && !Array.isArray(job) && typeof job === 'object') { + jobActions.setForm(job); + } }); return jobActions; }); From 891341532b95953d028edf7250c1798c973903da Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 25 Sep 2015 12:53:29 -0700 Subject: [PATCH 25/42] refactor form to do validation right in component --- common/app/routes/Jobs/components/NewJob.jsx | 135 ++++++++----------- common/app/routes/Jobs/flux/Actions.js | 47 +------ common/app/routes/Jobs/utils.js | 2 +- 3 files changed, 58 insertions(+), 126 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index b160671c4b..12f9b425a0 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -37,22 +37,31 @@ const checkValidity = [ 'highlight' ]; +function formatValue(value, validator, type = 'string') { + const formated = getDefaults(type); + if (validator && type === 'string') { + formated.valid = validator(value); + } + if (value) { + formated.value = value; + formated.bsStyle = formated.valid ? 'success' : 'error'; + } + return formated; +} + +function isValidURL(data) { + return isURL(data, { 'require_protocol': true }); +} + +function isValidPhone(data) { + return isMobilePhone(data, 'en-US'); +} + export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { const { - position = getDefaults('string'), - locale = getDefaults('string'), - description = getDefaults('string'), - email = getDefaults('string'), - phone = getDefaults('string'), - url = getDefaults('string'), - logo = getDefaults('string'), - name = getDefaults('string'), - highlight = getDefaults('bool') - } = form; - return { position, locale, description, @@ -62,6 +71,17 @@ export default contain({ logo, name, highlight + } = form; + return { + position: formatValue(position, isAscii), + locale: formatValue(locale, isAscii), + description: formatValue(description, isAscii), + email: formatValue(email, isEmail), + phone: formatValue(phone, isValidPhone), + url: formatValue(url, isValidURL), + logo: formatValue(logo, isValidURL), + name: formatValue(name, isAscii), + highlight: formatValue(highlight, null, 'bool') }; }, subscribeOnWillMount() { @@ -86,11 +106,12 @@ export default contain({ handleSubmit(e) { e.preventDefault(); + const props = this.props; let valid = true; checkValidity.forEach((prop) => { // if value exist, check if it is valid - if (this.props[prop].value) { - valid = valid && !!this.props[prop].valid; + if (props[prop].value && props[prop].type !== 'boolean') { + valid = valid && !!props[prop].valid; } }); @@ -141,9 +162,9 @@ export default contain({ jobActions.getSavedForm(); }, - handleChange(name, validator, { target: { value } }) { + handleChange(name, { target: { value } }) { const { jobActions: { handleForm } } = this.props; - handleForm({ name, value, validator }); + handleForm({ [name]: value }); }, render() { @@ -156,8 +177,10 @@ export default contain({ url, logo, name, - highlight + highlight, + jobActions: { handleForm } } = this.props; + const { handleChange } = this; const labelClass = 'col-sm-offset-1 col-sm-2'; const inputClass = 'col-sm-6'; @@ -178,13 +201,7 @@ export default contain({ bsStyle={ position.bsStyle } label='Position' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'position', - isAscii, - e - ); - }} + onChange={ (e) => handleChange('position', e) } placeholder='Position' type='text' value={ position.value } @@ -193,13 +210,7 @@ export default contain({ bsStyle={ locale.bsStyle } label='Location' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'locale', - isAscii, - e, - ); - }} + onChange={ (e) => handleChange('locale', e) } placeholder='Location' type='text' value={ locale.value } @@ -208,13 +219,7 @@ export default contain({ bsStyle={ description.bsStyle } label='Description' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'description', - isAscii, - e - ); - }} + onChange={ (e) => handleChange('description', e) } placeholder='Description' rows='10' type='textarea' @@ -228,13 +233,7 @@ export default contain({ bsStyle={ name.bsStyle } label='Company Name' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'name', - isAscii, - e, - ); - }} + onChange={ (e) => handleChange('name', e) } placeholder='Foo, INC' type='text' value={ name.value } @@ -243,13 +242,7 @@ export default contain({ bsStyle={ email.bsStyle } label='Email' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'email', - isEmail, - e - ); - }} + onChange={ (e) => handleChange('email', e) } placeholder='Email' type='email' value={ email.value } @@ -258,13 +251,7 @@ export default contain({ bsStyle={ phone.bsStyle } label='Phone' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'phone', - (data) => isMobilePhone(data, 'en-US'), - e - ); - }} + onChange={ (e) => handleChange('phone', e) } placeholder='555-123-1234' type='tel' value={ phone.value } @@ -273,13 +260,7 @@ export default contain({ bsStyle={ url.bsStyle } label='URL' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'url', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} + onChange={ (e) => handleChange('url', e) } placeholder='http://freecatphotoapp.com' type='url' value={ url.value } @@ -288,13 +269,7 @@ export default contain({ bsStyle={ logo.bsStyle } label='Logo' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'logo', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} + onChange={ (e) => handleChange('logo', e) } placeholder='http://freecatphotoapp.com/logo.png' type='url' value={ logo.value } @@ -304,17 +279,15 @@ export default contain({

Make it stand out

{ - this.handleChange( - 'highlight', - () => { return true; }, - e - ); - }} - type='checkbox' - value={ highlight.value } /> + onChange={ + ({ target: { checked } }) => handleForm({ + highlight: !!checked + }) + } + type='checkbox' />
{} }) { - if (!name) { - // operation noop - return { replace: null }; - } - if (!validator(value)) { - return { - transform(oldState) { - const { form } = oldState; - const newState = assign({}, oldState); - newState.form = assign( - {}, - form, - { - [name]: { - value, - valid: false, - pristine: false, - bsStyle: value ? 'error' : null - } - } - ); - return newState; - } - }; - } + handleForm(value) { return { transform(oldState) { const { form } = oldState; @@ -74,14 +48,7 @@ export default Actions({ newState.form = assign( {}, form, - { - [name]: { - value, - valid: true, - pristine: false, - bsStyle: value ? 'success' : null - } - } + value ); return newState; } @@ -89,15 +56,7 @@ export default Actions({ }, saveForm: null, getSavedForm: null, - setForm(job) { - const form = Object.keys(job).reduce((accu, prop) => { - console.log('form', accu); - return Object.assign( - accu, - { [prop]: getDefaults(typeof prop, job[prop]) } - ); - }, {}); - + setForm(form) { return { form }; } }) diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js index 3a60c373a8..aeb0396c12 100644 --- a/common/app/routes/Jobs/utils.js +++ b/common/app/routes/Jobs/utils.js @@ -18,5 +18,5 @@ export function getDefaults(type, value) { if (value) { return Object.assign({}, defaults[type], { value }); } - return defaults[type]; + return Object.assign({}, defaults[type]); } From 4a3283a5a7dc82471038005209f4c1d3b86357ed Mon Sep 17 00:00:00 2001 From: Aayush Kapoor Date: Sat, 26 Sep 2015 16:01:46 +0530 Subject: [PATCH 26/42] Edit last test case for Bonfire: Where art thou --- seed/challenges/intermediate-bonfires.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/intermediate-bonfires.json b/seed/challenges/intermediate-bonfires.json index d804919dfd..ab74b6cab8 100644 --- a/seed/challenges/intermediate-bonfires.json +++ b/seed/challenges/intermediate-bonfires.json @@ -153,7 +153,7 @@ "assert.deepEqual(where([{ first: 'Romeo', last: 'Montague' }, { first: 'Mercutio', last: null }, { first: 'Tybalt', last: 'Capulet' }], { last: 'Capulet' }), [{ first: 'Tybalt', last: 'Capulet' }], 'should return an array of objects');", "assert.deepEqual(where([{ 'a': 1 }, { 'a': 1 }, { 'a': 1, 'b': 2 }], { 'a': 1 }), [{ 'a': 1 }, { 'a': 1 }, { 'a': 1, 'b': 2 }], 'should return with multiples');", "assert.deepEqual(where([{ 'a': 1, 'b': 2 }, { 'a': 1 }, { 'a': 1, 'b': 2, 'c': 2 }], { 'a': 1, 'b': 2 }), [{ 'a': 1, 'b': 2 }, { 'a': 1, 'b': 2, 'c': 2 }], 'should return two objects in array');", - "assert.deepEqual(where([{ 'a': 5 }, { 'a': 5 }, { 'a': 5, 'b': 10 }], { 'a': 5, 'b': 10 }), [{ 'a': 5, 'b': 10 }], 'should return a single object in array');" + "assert.deepEqual(where([{ 'a': 5 }, { 'b': 10 }, { 'a': 5, 'b': 10 }], { 'a': 5, 'b': 10 }), [{ 'a': 5, 'b': 10 }], 'should return a single object in array');" ], "MDNlinks": [ "Global Object", From 5d0cb58d3a161f50aab0fc852c97ea3dcbdefbda Mon Sep 17 00:00:00 2001 From: natac13 Date: Sat, 26 Sep 2015 11:17:49 -0400 Subject: [PATCH 27/42] fixed typo in test closes #3505 --- seed/challenges/basic-bonfires.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/challenges/basic-bonfires.json b/seed/challenges/basic-bonfires.json index df356f9034..34c4c809ad 100644 --- a/seed/challenges/basic-bonfires.json +++ b/seed/challenges/basic-bonfires.json @@ -464,9 +464,9 @@ "slasher([1, 2, 3], 2, \"\");" ], "tests": [ - "assert.deepEqual(slasher([1, 2, 3], 2), [3], '[1, 2, 3], 2, [3] should return [3].');", - "assert.deepEqual(slasher([1, 2, 3], 0), [1, 2, 3], '[1, 2, 3], 0 should return [1, 2, 3].');", - "assert.deepEqual(slasher([1, 2, 3], 9), [], '[1, 2, 3], 9 should return [].');" + "assert.deepEqual(slasher([1, 2, 3], 2), [3], 'slasher([1, 2, 3], 2) should return [3].');", + "assert.deepEqual(slasher([1, 2, 3], 0), [1, 2, 3], 'slasher([1, 2, 3], 0) should return [1, 2, 3].');", + "assert.deepEqual(slasher([1, 2, 3], 9), [], 'slasher([1, 2, 3], 9) should return [].');" ], "MDNlinks": [ "Array.slice()", From 90f6d986d783a9d5b6931dff89d1ef846b1766b4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 26 Sep 2015 22:23:56 -0700 Subject: [PATCH 28/42] show preview from new job --- common/app/routes/Jobs/components/NewJob.jsx | 6 ++ common/app/routes/Jobs/components/Preview.jsx | 14 ++++ common/app/routes/Jobs/components/Show.jsx | 68 +------------------ common/app/routes/Jobs/components/ShowJob.jsx | 67 ++++++++++++++++++ common/app/routes/Jobs/index.js | 4 ++ 5 files changed, 93 insertions(+), 66 deletions(-) create mode 100644 common/app/routes/Jobs/components/Preview.jsx create mode 100644 common/app/routes/Jobs/components/ShowJob.jsx diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 12f9b425a0..4c369411f0 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import { History } from 'react-router'; import { contain } from 'thundercats-react'; import debugFactory from 'debug'; import { getDefaults } from '../utils'; @@ -104,6 +105,8 @@ export default contain({ highlight: PropTypes.object }, + mixins: [History], + handleSubmit(e) { e.preventDefault(); const props = this.props; @@ -153,8 +156,11 @@ export default contain({ return accu; }, {}); + job.postedOn = new Date(); debug('job sanitized', job); jobActions.saveForm(job); + + this.history.pushState(null, '/jobs/new/preview'); }, componentDidMount() { diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx new file mode 100644 index 0000000000..5b6081be5c --- /dev/null +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -0,0 +1,14 @@ +// import React, { PropTypes } from 'react'; +import { contain } from 'thundercats-react'; +import ShowJob from './ShowJob.jsx'; + +export default contain( + { + store: 'JobsStore', + actions: 'JobActions', + map({ form: job = {} }) { + return { job }; + } + }, + ShowJob +); diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 0baedb82b3..ce2512c27d 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -1,13 +1,5 @@ -import React, { PropTypes } from 'react'; import { contain } from 'thundercats-react'; -import { Row, Thumbnail, Panel, Well } from 'react-bootstrap'; -import moment from 'moment'; - -const thumbnailStyle = { - backgroundColor: 'white', - maxHeight: '100px', - maxWidth: '100px' -}; +import ShowJob from './ShowJob.jsx'; export default contain( { @@ -28,61 +20,5 @@ export default contain( return job.id !== id; } }, - React.createClass({ - displayName: 'ShowJob', - propTypes: { - job: PropTypes.object, - params: PropTypes.object - }, - - renderHeader({ company, position }) { - return ( -
-

{ company }

-
- { position } -
-
- ); - }, - - render() { - const { job = {} } = this.props; - const { - logo, - position, - city, - company, - state, - email, - phone, - postedOn, - description - } = job; - - return ( -
- - - - - Position: { position } - Location: { city }, { state } -
- Contact: { email || phone || 'N/A' } -
- Posted On: { moment(postedOn).format('MMMM Do, YYYY') } -
-

{ description }

-
-
-
- ); - } - }) + ShowJob ); diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx new file mode 100644 index 0000000000..1a048a3fff --- /dev/null +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -0,0 +1,67 @@ +import React, { PropTypes } from 'react'; +import { Row, Thumbnail, Panel, Well } from 'react-bootstrap'; +import moment from 'moment'; + +const thumbnailStyle = { + backgroundColor: 'white', + maxHeight: '100px', + maxWidth: '100px' +}; + +export default React.createClass({ + displayName: 'ShowJob', + propTypes: { + job: PropTypes.object, + params: PropTypes.object + }, + + renderHeader({ company, position }) { + return ( +
+

{ company }

+
+ { position } +
+
+ ); + }, + + render() { + const { job = {} } = this.props; + const { + logo, + position, + city, + company, + state, + email, + phone, + postedOn, + description + } = job; + + return ( +
+ + + + + Position: { position } + Location: { city }, { state } +
+ Contact: { email || phone || 'N/A' } +
+ Posted On: { moment(postedOn).format('MMMM Do, YYYY') } +
+

{ description }

+
+
+
+ ); + } +}); diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index 3564332d32..6c556c994e 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -1,6 +1,7 @@ import Jobs from './components/Jobs.jsx'; import NewJob from './components/NewJob.jsx'; import Show from './components/Show.jsx'; +import Preview from './components/Preview.jsx'; /* * index: /jobs list jobs @@ -15,6 +16,9 @@ export default { }, { path: 'jobs/new', component: NewJob + }, { + path: 'jobs/new/preview', + component: Preview }, { path: 'jobs/:id', component: Show From 0634926af0228412b4da7bd9a89ad0a4251fa274 Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Sun, 27 Sep 2015 01:44:09 -0700 Subject: [PATCH 29/42] add t-shirts to view --- server/views/challengeMap/show.jade | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 05d591b010..72e358261f 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -17,7 +17,37 @@ block content | since we launched   span.text-primary #{daysRunning}   | days ago. - a.btn.btn-lg.signup-btn.btn-block(href="https://www.facebook.com/sharer/sharer.php?u=http://freecodecamp.com" target='_blank') Share our open source community on Facebook and help us grow. + .spacer + h3.text-center Vote for the T-shirt design you like the most. + h4.text-center We'll announce the winning design on Saturday's Summit on Twitch.tv, and it will become our community's first official t-shirt. + .row + .col-xs-6.thumbnail + a(href="http://i.imgur.com/LlXGa5y.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/LlXGa5y.png' alt="t-shirt option 1 women's") + .caption + p.text-center Design 1 Women's + .col-xs-6.thumbnail + a(href="http://i.imgur.com/aefwnnv.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/aefwnnv.png' alt="t-shirt option 2 women's") + .caption + p.text-center Design 2 Women's + .row + .col-xs-6.thumbnail + a(href="http://i.imgur.com/aYH0aqf.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/aYH0aqf.png' alt="t-shirt option 1 men's") + .caption + p.text-center Design 1 Men's + .col-xs-6.thumbnail + a(href="http://i.imgur.com/v9KlV4g.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/v9KlV4g.png' alt="t-shirt option 2 men's") + .caption + p.text-center Design 2 Men's + .row + .col-xs-6 + .button.btn.btn-block.btn-primary Vote for Design 1 + .col-xs-6 + .button.btn.btn-block.btn-primary Vote for Design 2 + .spacer .row .col-xs-12.col-sm-8.col-sm-offset-2 h3 800 Hours of Practice: From ccf5504537dafb84bf6195282fd80db552edb7cb Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Sun, 27 Sep 2015 10:49:44 -0700 Subject: [PATCH 30/42] add tshirt voting mechanism --- common/models/user.json | 3 ++ server/boot/user.js | 34 ++++++++++++++++++ server/views/challengeMap/show.jade | 55 +++++++++++++---------------- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/common/models/user.json b/common/models/user.json index d0d8bed6e8..647a64836d 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -157,6 +157,9 @@ "rand": { "type": "number", "index": true + }, + "tshirtVote": { + "type": "number" } }, "validations": [], diff --git a/server/boot/user.js b/server/boot/user.js index 33a80c5c62..881928f9fb 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -76,6 +76,8 @@ module.exports = function(app) { ); router.get('/account/unlink/:provider', getOauthUnlink); router.get('/account', getAccount); + router.get('/vote1', vote1); + router.get('/vote2', vote1); // Ensure this is the last route! router.get('/:username', returnUser); @@ -332,4 +334,36 @@ module.exports = function(app) { }); }); } + + function vote1(req, res) { + if (req.user) { + req.user.tshirtVote = 1; + req.user.save(function (err) { + if (err) { + return next(err); + } + req.flash('success', {msg: 'Thanks for voting!'}); + res.redirect('/map'); + }); + } else { + req.flash('error', {msg: 'You must be signed in to vote.'}); + res.redirect('/map'); + } + } + + function vote2(req, res) { + if (req.user) { + req.user.tshirtVote = 2; + req.user.save(function (err) { + if (err) { + return next(err); + } + req.flash('success', {msg: 'Thanks for voting!'}); + res.redirect('/map'); + }); + } else { + req.flash('error', {msg: 'You must be signed in to vote.'}); + res.redirect('/map'); + } + } }; diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 72e358261f..8ac788c2bf 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -18,36 +18,31 @@ block content span.text-primary #{daysRunning}   | days ago. .spacer - h3.text-center Vote for the T-shirt design you like the most. - h4.text-center We'll announce the winning design on Saturday's Summit on Twitch.tv, and it will become our community's first official t-shirt. - .row - .col-xs-6.thumbnail - a(href="http://i.imgur.com/LlXGa5y.png" data-lightbox="img-enlarge") - img.img-responsive(src='http://i.imgur.com/LlXGa5y.png' alt="t-shirt option 1 women's") - .caption - p.text-center Design 1 Women's - .col-xs-6.thumbnail - a(href="http://i.imgur.com/aefwnnv.png" data-lightbox="img-enlarge") - img.img-responsive(src='http://i.imgur.com/aefwnnv.png' alt="t-shirt option 2 women's") - .caption - p.text-center Design 2 Women's - .row - .col-xs-6.thumbnail - a(href="http://i.imgur.com/aYH0aqf.png" data-lightbox="img-enlarge") - img.img-responsive(src='http://i.imgur.com/aYH0aqf.png' alt="t-shirt option 1 men's") - .caption - p.text-center Design 1 Men's - .col-xs-6.thumbnail - a(href="http://i.imgur.com/v9KlV4g.png" data-lightbox="img-enlarge") - img.img-responsive(src='http://i.imgur.com/v9KlV4g.png' alt="t-shirt option 2 men's") - .caption - p.text-center Design 2 Men's - .row - .col-xs-6 - .button.btn.btn-block.btn-primary Vote for Design 1 - .col-xs-6 - .button.btn.btn-block.btn-primary Vote for Design 2 - .spacer + if (user && !user.tshirtVote) + h3.text-center Vote for the T-shirt design you like the most. + h4.text-center We'll announce the winning design on Saturday's Summit on Twitch.tv, and it will become our community's first official t-shirt. + .row + .col-xs-6 + a(href="http://i.imgur.com/LlXGa5y.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/LlXGa5y.png' alt="t-shirt option 1 women's") + .col-xs-6 + a(href="http://i.imgur.com/aefwnnv.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/aefwnnv.png' alt="t-shirt option 2 women's") + .button-spacer + .row + .col-xs-6 + a(href="http://i.imgur.com/aYH0aqf.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/aYH0aqf.png' alt="t-shirt option 1 men's") + .col-xs-6 + a(href="http://i.imgur.com/v9KlV4g.png" data-lightbox="img-enlarge") + img.img-responsive(src='http://i.imgur.com/v9KlV4g.png' alt="t-shirt option 2 men's") + .button-spacer + .row + .col-xs-6 + a.button.btn.btn-block.btn-primary(href='/vote1') Vote for Design 1 + .col-xs-6 + a.button.btn.btn-block.btn-primary(href='/vote2') Vote for Design 2 + .spacer .row .col-xs-12.col-sm-8.col-sm-offset-2 h3 800 Hours of Practice: From a64a46cff1e6e93500aea987f6226a9d56020450 Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Sun, 27 Sep 2015 11:05:44 -0700 Subject: [PATCH 31/42] don't show tshirt voting to brand new users --- server/views/challengeMap/show.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 8ac788c2bf..35ec5886c5 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -18,7 +18,7 @@ block content span.text-primary #{daysRunning}   | days ago. .spacer - if (user && !user.tshirtVote) + if (user && !user.tshirtVote && user.progressTimestamps.length > 5) h3.text-center Vote for the T-shirt design you like the most. h4.text-center We'll announce the winning design on Saturday's Summit on Twitch.tv, and it will become our community's first official t-shirt. .row From 305f66119605e33b6814a29e7d79207d5a4d16a9 Mon Sep 17 00:00:00 2001 From: Abhisek Pattnaik Date: Mon, 28 Sep 2015 01:04:59 +0530 Subject: [PATCH 32/42] Correct comment Replace "ourArray[1]" with "ourArray" close FreeCodeCamp/FreeCodeCamp#3515 --- seed/challenges/basic-javascript.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seed/challenges/basic-javascript.json b/seed/challenges/basic-javascript.json index e3542ee41d..e0b38cc841 100644 --- a/seed/challenges/basic-javascript.json +++ b/seed/challenges/basic-javascript.json @@ -520,7 +520,7 @@ "challengeSeed":[ "var ourArray = [1,2,3];", "ourArray[1] = 3;", - "// ourArray[1] now equals [1,3,3].", + "// ourArray now equals [1,3,3].", "var myArray = [1,2,3];", "// Only change code below this line.", "", From 5b5b37adff62b0582fd5d8535b54e6b5f751ee2e Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Sun, 27 Sep 2015 14:00:55 -0700 Subject: [PATCH 33/42] fix tshirt version 2 not submitting properly bug --- server/boot/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/boot/user.js b/server/boot/user.js index 881928f9fb..9c6450db9c 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -77,7 +77,7 @@ module.exports = function(app) { router.get('/account/unlink/:provider', getOauthUnlink); router.get('/account', getAccount); router.get('/vote1', vote1); - router.get('/vote2', vote1); + router.get('/vote2', vote2); // Ensure this is the last route! router.get('/:username', returnUser); From 0ef3e36d6f72dbfde29bec16fe988decee38f73c Mon Sep 17 00:00:00 2001 From: Abhisek Pattnaik Date: Mon, 28 Sep 2015 03:45:13 +0530 Subject: [PATCH 34/42] Fix comments in Waypoint: Manipulate Arrays With unshift --- seed/challenges/basic-javascript.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/seed/challenges/basic-javascript.json b/seed/challenges/basic-javascript.json index e3542ee41d..8e396d3ec9 100644 --- a/seed/challenges/basic-javascript.json +++ b/seed/challenges/basic-javascript.json @@ -643,8 +643,9 @@ "challengeSeed": [ "var ourArray = [\"Stimpson\", \"J\", [\"cat\"]];", "ourArray.shift();", - "// ourArray now equals [\"happy\", \"J\", [\"cat\"]]", + "// ourArray now equals [\"J\", [\"cat\"]]", "ourArray.unshift(\"happy\");", + "// ourArray now equals [\"happy\", \"J\", [\"cat\"]]", "", "var myArray = [\"John\", 23, [\"dog\", 3]];", "myArray.shift();", From 85a5766546d617c5c2b6901faa4b504661a0d67d Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Sun, 27 Sep 2015 16:25:34 -0700 Subject: [PATCH 35/42] update the tshirt campaign text --- server/views/challengeMap/show.jade | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 35ec5886c5..11d14bceee 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -20,7 +20,9 @@ block content .spacer if (user && !user.tshirtVote && user.progressTimestamps.length > 5) h3.text-center Vote for the T-shirt design you like the most. - h4.text-center We'll announce the winning design on Saturday's Summit on Twitch.tv, and it will become our community's first official t-shirt. + h4.text-center We'll announce the winning design during our Summit on Saturday at Noon EST on  + a(href='https://twitch.tv/freecodecamp' target='_blank') Twitch.tv + |  and it will become our community's first official t-shirt (in women's and men's sizes). .row .col-xs-6 a(href="http://i.imgur.com/LlXGa5y.png" data-lightbox="img-enlarge") @@ -39,9 +41,11 @@ block content .button-spacer .row .col-xs-6 - a.button.btn.btn-block.btn-primary(href='/vote1') Vote for Design 1 + h3.text-center "Minified JavaScript Logo" + a.button.btn.btn-block.btn-primary(href='/vote1') Vote for this Design .col-xs-6 - a.button.btn.btn-block.btn-primary(href='/vote2') Vote for Design 2 + h3.text-center "Function Call Logo" + a.button.btn.btn-block.btn-primary(href='/vote2') Vote for this design .spacer .row .col-xs-12.col-sm-8.col-sm-offset-2 From 5fc97da3c3e186754e2d2ee5b299c1a21df48fcf Mon Sep 17 00:00:00 2001 From: Aniruddh Agarwal Date: Mon, 21 Sep 2015 22:01:10 +0800 Subject: [PATCH 36/42] Add test HTML5 waypoint to make it stricter - Added a test to the "Nest an Anchor Element within a Paragraph" waypoint. The text 'View more' must now be outside the tags. --- seed/challenges/html5-and-css.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/seed/challenges/html5-and-css.json b/seed/challenges/html5-and-css.json index 051b6764c7..d865ce2d56 100644 --- a/seed/challenges/html5-and-css.json +++ b/seed/challenges/html5-and-css.json @@ -1142,7 +1142,8 @@ "assert($(\"a\").text().match(/cat\\sphotos/gi), 'Your a element should have the anchor text of \"cat photos\"')", "assert($(\"p\") && $(\"p\").length > 2, 'Create a new p element around your a element.')", "assert($(\"a[href=\\\"http://www.freecatphotoapp.com\\\"]\").parent().is(\"p\"), 'Your a element should be nested within your new p element.')", - "assert($(\"p\").text().match(/View\\smore/gi), 'Your p element should have the text \"View more\".')", + "assert($(\"p\").text().match(/^View\\smore\\s/gi), 'Your p element should have the text \"View more \" (with a space after it).')", + "assert(!$(\"a\").text().match(/View\\smore/gi), 'Your a element should not have the text \"View more\".')", "assert(editor.match(/<\\/p>/g) && editor.match(/

/g).length === editor.match(/

p elements has a closing tag.')", "assert(editor.match(/<\\/a>/g) && editor.match(//g).length === editor.match(/a elements has a closing tag.')" ], From 91efd5940d733b377f49b3cf7c0afcd37f651463 Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Tue, 22 Sep 2015 13:17:48 -0700 Subject: [PATCH 37/42] add facebook sharing to map on completed challenges remove twitter share from non-bonfire challenges --- client/main.js | 53 +++++++++++++++++++++++++ server/views/challengeMap/show.jade | 9 ++++- server/views/coursewares/showJS.jade | 4 -- server/views/coursewares/showVideo.jade | 6 --- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/client/main.js b/client/main.js index 6e6ce8c696..5128ddb5e4 100644 --- a/client/main.js +++ b/client/main.js @@ -1,3 +1,29 @@ +var mapShareKey = 'map-shares'; + +function getMapShares() { + var alreadyShared = JSON.parse(localStorage.getItem(mapShareKey) || '[]'); + if (!alreadyShared || !Array.isArray(alreadyShared)) { + localStorage.setItem(mapShareKey, JSON.stringify([])); + alreadyShared = []; + } + return alreadyShared; +} + +function setMapShare(id) { + var alreadyShared = getMapShares(); + var found = false; + alreadyShared.forEach(function(_id) { + if (_id === id) { + found = true; + } + }); + if (!found) { + alreadyShared.push(id); + } + localStorage.setItem(mapShareKey, JSON.stringify(alreadyShared)); + return alreadyShared; +} + $(document).ready(function() { var challengeName = typeof challengeName !== 'undefined' ? @@ -383,6 +409,33 @@ $(document).ready(function() { } }, false); } + + + // map sharing + var alreadyShared = getMapShares(); + alreadyShared.map(function(id) { + // find share button div and hide as camper has already shared + $('div[id="' + id + '"]').parent().parent().hide(); + }); + + // on map view + $('.map-challenge-block-share').on('click', function(e) { + e.preventDefault(); + var challengeBlockName = $(this).children().attr('id'); + var challengeBlockEscapedName = challengeBlockName.replace(/\s/, '%20'); + + var link = 'https://www.facebook.com/dialog/feed?' + + 'app_id=1644598365767721' + + '&display=page&' + + 'caption=I%20just%20completed%20' + + challengeBlockEscapedName + + '%20on%20Free%20Code%20Camp' + + '&link=http%3A%2F%2Ffreecodecamp.com' + + '&redirect_uri=http://freecodecamp.com/map'; + + setMapShare(challengeBlockName); + window.location.href = link; + }); }); function defCheck(a){ diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 11d14bceee..a7408ada09 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -131,7 +131,14 @@ block content span= challenge.title span.sr-only= " Incomplete" - //#announcementModal.modal(tabindex='-1') + if (challengeBlock.completed === 100) + .button-spacer + .row + .col-xs-12.col-sm-8.col-md-6.col-sm-offset-3.col-md-offset-2 + a.btn.btn-lg.btn-block.signup-btn.map-challenge-block-share Section complete! Tell your friends. + .hidden(id="#{challengeBlock.name}") + + // #announcementModal.modal(tabindex='-1') // .modal-dialog.animated.fadeInUp.fast-animation // .modal-content // .modal-header.challenge-list-header Add us to your LinkedIn profile diff --git a/server/views/coursewares/showJS.jade b/server/views/coursewares/showJS.jade index 03d6b823e1..f4621b5b28 100644 --- a/server/views/coursewares/showJS.jade +++ b/server/views/coursewares/showJS.jade @@ -94,10 +94,6 @@ block content .row if (user) #submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter) - if (user.progressTimestamps.length > 2) - a.btn.btn-lg.btn-block.btn-twitter(target="_blank", href="https://twitter.com/intent/tweet?text=I%20just%20#{verb}%20%40FreeCodeCamp%20#{name}&url=http%3A%2F%2Ffreecodecamp.com/challenges/#{dashedName}&hashtags=LearnToCode, JavaScript") - i.fa.fa-twitter   - = phrase else a.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) Go to my next challenge include ../partials/challenge-modals diff --git a/server/views/coursewares/showVideo.jade b/server/views/coursewares/showVideo.jade index 30ad2d5f1a..bd7f42e5ed 100644 --- a/server/views/coursewares/showVideo.jade +++ b/server/views/coursewares/showVideo.jade @@ -69,12 +69,6 @@ block content a.btn.btn-lg.btn-primary.btn-block#next-courseware-button(name='_csrf', value=_csrf) I've completed this challenge (ctrl + enter) script. $('#complete-courseware-editorless-dialog').bind('keypress', modalControlEnterHandler); - - if (user.progressTimestamps.length > 2) - .button-spacer - a.btn.btn-lg.btn-block.btn-twitter(href="https://twitter.com/intent/tweet?text=I%20just%20#{verb}%20%40FreeCodeCamp%20#{name}&url=http%3A%2F%2Ffreecodecamp.com/challenges/#{dashedName}&hashtags=LearnToCode, JavaScript" target="_blank") - i.fa.fa-twitter   - = phrase else a.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) I've completed this challenge (ctrl + enter) script. From 1ff55374dbd2b56bc9b4cfceb3a0afb86b676865 Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Mon, 28 Sep 2015 19:04:44 -0700 Subject: [PATCH 38/42] improve facebook sharing copy --- client/main.js | 11 ++++++----- server/views/challengeMap/show.jade | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/client/main.js b/client/main.js index 5128ddb5e4..47a9905394 100644 --- a/client/main.js +++ b/client/main.js @@ -423,16 +423,17 @@ $(document).ready(function() { e.preventDefault(); var challengeBlockName = $(this).children().attr('id'); var challengeBlockEscapedName = challengeBlockName.replace(/\s/, '%20'); + var username = typeof window.username !== "undefined" ? window.username : ""; var link = 'https://www.facebook.com/dialog/feed?' + 'app_id=1644598365767721' + '&display=page&' + - 'caption=I%20just%20completed%20' + + 'caption=I%20just%20completed%20the%20' + challengeBlockEscapedName + - '%20on%20Free%20Code%20Camp' + - '&link=http%3A%2F%2Ffreecodecamp.com' + - '&redirect_uri=http://freecodecamp.com/map'; - + '%20section%20on%20Free%20Code%20Camp%2E%20Check%20out%20my%20portfolio%20so%20far%2E' + + '&link=http%3A%2F%2Ffreecodecamp%2Ecom%2F' + + username + + '&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap'; setMapShare(challengeBlockName); window.location.href = link; }); diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index a7408ada09..4ac3064426 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -135,9 +135,10 @@ block content .button-spacer .row .col-xs-12.col-sm-8.col-md-6.col-sm-offset-3.col-md-offset-2 - a.btn.btn-lg.btn-block.signup-btn.map-challenge-block-share Section complete! Tell your friends. + a.btn.btn-lg.btn-block.signup-btn.map-challenge-block-share Section complete. Share your Portfolio with your friends. .hidden(id="#{challengeBlock.name}") - + script. + var username = !{JSON.stringify(user && user.username || "")}; // #announcementModal.modal(tabindex='-1') // .modal-dialog.animated.fadeInUp.fast-animation // .modal-content From 6a5bea67f7cb1d47050669a7a412d35f0a3341dc Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 28 Sep 2015 19:20:29 -0700 Subject: [PATCH 39/42] set last completed block in map view --- server/boot/challenge.js | 10 +++++++++- server/views/challengeMap/show.jade | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/server/boot/challenge.js b/server/boot/challenge.js index 15736e3a51..239d21d4d0 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -471,6 +471,7 @@ module.exports = function(app) { } function challengeMap({ user = {} }, res, next) { + let lastCompleted; const daysRunning = moment().diff(new Date('10/15/2014'), 'days'); // if user @@ -513,7 +514,13 @@ module.exports = function(app) { }) .filter(({ name }) => name !== 'Hikes') // turn stream of blocks into a stream of an array - .toArray(); + .toArray() + .doOnNext((blocks) => { + const lastCompletedBlock = _.findLast(blocks, (block) => { + return block.completed === 100; + }); + lastCompleted = lastCompletedBlock.name; + }); Observable.combineLatest( camperCount$, @@ -526,6 +533,7 @@ module.exports = function(app) { blocks, daysRunning, camperCount, + lastCompleted, title: "A map of all Free Code Camp's Challenges" }); }, diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 4ac3064426..21515a3613 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -138,7 +138,8 @@ block content a.btn.btn-lg.btn-block.signup-btn.map-challenge-block-share Section complete. Share your Portfolio with your friends. .hidden(id="#{challengeBlock.name}") script. - var username = !{JSON.stringify(user && user.username || "")}; + var username = !{JSON.stringify(user && user.username || '')}; + var lastCompleted = !{JSON.stringify(lastCompleted || false)} // #announcementModal.modal(tabindex='-1') // .modal-dialog.animated.fadeInUp.fast-animation // .modal-content From c7d68932ce8f1763e89b5e375333703bd1c729ce Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 28 Sep 2015 19:47:45 -0700 Subject: [PATCH 40/42] Show share button from last completed --- client/main.js | 22 ++++++++++++++++------ server/views/challengeMap/show.jade | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/client/main.js b/client/main.js index 47a9905394..bdde23f436 100644 --- a/client/main.js +++ b/client/main.js @@ -1,4 +1,7 @@ var mapShareKey = 'map-shares'; +var lastCompleted = typeof lastCompleted !== 'undefined' ? + lastCompleted : + ''; function getMapShares() { var alreadyShared = JSON.parse(localStorage.getItem(mapShareKey) || '[]'); @@ -413,27 +416,34 @@ $(document).ready(function() { // map sharing var alreadyShared = getMapShares(); - alreadyShared.map(function(id) { - // find share button div and hide as camper has already shared - $('div[id="' + id + '"]').parent().parent().hide(); - }); + + if (lastCompleted && alreadyShared.indexOf(lastCompleted) === -1) { + $('div[id="' + lastCompleted + '"]') + .parent() + .parent() + .removeClass('hidden'); + } // on map view $('.map-challenge-block-share').on('click', function(e) { e.preventDefault(); var challengeBlockName = $(this).children().attr('id'); var challengeBlockEscapedName = challengeBlockName.replace(/\s/, '%20'); - var username = typeof window.username !== "undefined" ? window.username : ""; + var username = typeof window.username !== 'undefined' ? + window.username : + ''; var link = 'https://www.facebook.com/dialog/feed?' + 'app_id=1644598365767721' + '&display=page&' + 'caption=I%20just%20completed%20the%20' + challengeBlockEscapedName + - '%20section%20on%20Free%20Code%20Camp%2E%20Check%20out%20my%20portfolio%20so%20far%2E' + + '%20section%20on%20Free%20Code%20Camp%2E%20Check%20out' + + '%20my%20portfolio%20so%20far%2E' + '&link=http%3A%2F%2Ffreecodecamp%2Ecom%2F' + username + '&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap'; + setMapShare(challengeBlockName); window.location.href = link; }); diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 21515a3613..42fa90422b 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -134,7 +134,7 @@ block content if (challengeBlock.completed === 100) .button-spacer .row - .col-xs-12.col-sm-8.col-md-6.col-sm-offset-3.col-md-offset-2 + .col-xs-12.col-sm-8.col-md-6.col-sm-offset-3.col-md-offset-2.hidden a.btn.btn-lg.btn-block.signup-btn.map-challenge-block-share Section complete. Share your Portfolio with your friends. .hidden(id="#{challengeBlock.name}") script. From 55de772aded1047091d9e5e64fe874edb55fb78f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 28 Sep 2015 20:05:22 -0700 Subject: [PATCH 41/42] Remove portfolio comment from share --- client/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/main.js b/client/main.js index bdde23f436..c1578c1d6f 100644 --- a/client/main.js +++ b/client/main.js @@ -438,8 +438,7 @@ $(document).ready(function() { '&display=page&' + 'caption=I%20just%20completed%20the%20' + challengeBlockEscapedName + - '%20section%20on%20Free%20Code%20Camp%2E%20Check%20out' + - '%20my%20portfolio%20so%20far%2E' + + '%20section%20on%20Free%20Code%20Camp%2E' + '&link=http%3A%2F%2Ffreecodecamp%2Ecom%2F' + username + '&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap'; From ee9827ea587e38c3cd3e6ae5978766832b71d336 Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Tue, 29 Sep 2015 17:59:16 -0700 Subject: [PATCH 42/42] fix issues with get-started and remove pair button --- server/views/coursewares/showBonfire.jade | 41 ------------------- .../coursewares/showZiplineOrBasejump.jade | 3 -- server/views/partials/challenge-modals.jade | 14 ------- server/views/resources/get-started.jade | 24 ++--------- 4 files changed, 4 insertions(+), 78 deletions(-) diff --git a/server/views/coursewares/showBonfire.jade b/server/views/coursewares/showBonfire.jade index 52ec4a097b..e64424e1ac 100644 --- a/server/views/coursewares/showBonfire.jade +++ b/server/views/coursewares/showBonfire.jade @@ -21,44 +21,6 @@ block content .innerMarginFix(style=' width: 99%') #testCreatePanel.well h3.text-center.negative-10= name - .positive-15.positive-15-bottom - h4.text-center.bonfire-flames Difficulty:  - if (difficulty == "0") - i.ion-ios-flame-outline - i.ion-ios-flame-outline - i.ion-ios-flame-outline - i.ion-ios-flame-outline - i.ion-ios-flame-outline - if (difficulty == "1") - i.ion-ios-flame - i.ion-ios-flame-outline - i.ion-ios-flame-outline - i.ion-ios-flame-outline - i.ion-ios-flame-outline - if (difficulty == "2") - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame-outline - i.ion-ios-flame-outline - i.ion-ios-flame-outline - if (difficulty == "3") - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame-outline - i.ion-ios-flame-outline - if (difficulty == "4") - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame-outline - if (difficulty == "5") - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame - i.ion-ios-flame .row .col-xs-12 .bonfire-instructions @@ -93,9 +55,6 @@ block content label.btn.btn-success#trigger-help-modal i.fa.fa-medkit |   Help - label.btn.btn-success#trigger-pair-modal - i.fa.fa-user-plus - |   Pair label.btn.btn-success#trigger-issue-modal i.fa.fa-bug |   Bug diff --git a/server/views/coursewares/showZiplineOrBasejump.jade b/server/views/coursewares/showZiplineOrBasejump.jade index 58f84f1c77..174be42e3e 100644 --- a/server/views/coursewares/showZiplineOrBasejump.jade +++ b/server/views/coursewares/showZiplineOrBasejump.jade @@ -27,9 +27,6 @@ block content .btn.btn-success.btn-big#trigger-help-modal i.fa.fa-medkit |   Help - .btn.btn-success.btn-big#trigger-pair-modal - i.fa.fa-user-plus - |   Pair .btn.btn-success.btn-big#trigger-issue-modal i.fa.fa-bug |   Bug diff --git a/server/views/partials/challenge-modals.jade b/server/views/partials/challenge-modals.jade index 4503e2ac17..746c1a6929 100644 --- a/server/views/partials/challenge-modals.jade +++ b/server/views/partials/challenge-modals.jade @@ -1,17 +1,3 @@ -#pair-modal.modal(tabindex='-1') - .modal-dialog.animated.fadeIn.fast-animation - .modal-content - .modal-header.challenge-list-header Ready to pair program? - a.close.closing-x(href='#', data-dismiss='modal', aria-hidden='true') × - .modal-body.text-center - h3 This will take you to our pair programming room where you can request a pair. - h3 You'll need   - a(href='//github.com/FreeCodeCamp/freecodecamp/wiki/How-to-install-Screenhero' target='_blank') Screenhero - | . - h3 Other campers may then message you about pair programming. - a.btn.btn-lg.btn-primary.btn-block.close-modal(href='https://gitter.im/FreeCodeCamp/LetsPair', target='_blank') Take me to the pair programming room - a.btn.btn-lg.btn-info.btn-block(href='#', data-dismiss='modal', aria-hidden='true') Cancel - #issue-modal.modal(tabindex='-1') .modal-dialog.animated.fadeIn.fast-animation .modal-content diff --git a/server/views/resources/get-started.jade b/server/views/resources/get-started.jade index a3d19984f8..8767841a66 100644 --- a/server/views/resources/get-started.jade +++ b/server/views/resources/get-started.jade @@ -65,7 +65,7 @@ block content .big-spacer .thumbnail - img.gif-block.img-center.img-responsive(src='http://i.imgur.com/EZHzKCV.gif' alt='A gif showing you how to click the link below to go to our chat room and click the \"sign in with GitHub\" button. Then you can click into the text input field and type a message to your fellow campers.') + img.gif-block.img-center.img-responsive(src='http://i.imgur.com/Uuc2Ked.gif' alt='A gif showing you how to click the link below to go to our chat room and click the \"sign in with GitHub\" button. Then you can click into the text input field and type a message to your fellow campers.') .caption p.large-p Try this:  | Now that you have a GitHub account, you can  @@ -99,12 +99,6 @@ block content a(href='https://gitter.im/apps' target='_blank') download the chat room app |  to your computer or phone. - .big-spacer - .thumbnail - img.gif-block.img-center.img-responsive(src='http://i.imgur.com/DoOqkNW.gif' alt='A gif showing how you can click the "Wiki" button in your upper-right corner to access the wiki.') - .caption - p.large-p Try this: Click the "Wiki" button in your upper right hand corner. Our community has contributed lots of useful information to this searchable wiki. - .big-spacer .thumbnail img.gif-block.img-center.img-responsive(src='http://i.imgur.com/FkEzbto.gif' alt='A gif showing how you can click your profile image in your upper right hand corner to access the account page and connect GitHub.') @@ -128,25 +122,15 @@ block content .thumbnail img.gif-block.img-center.img-responsive(src='http://i.imgur.com/Elb3dfj.jpg' alt='A picture of some of our campers meeting in a local cafe. 3 men and 3 women are sitting around a table with laptops out, and are smiling and coding.') .caption - p.large-p Our Campsites help you code with campers in your city. You can coordinate study groups or attend local coding events together. + p.large-p Our Campsites help you code with campers in your city. You can discuss coding and attend local Coffee-n-Code events. .big-spacer .thumbnail - img.gif-block.img-center.img-responsive(src='http://i.imgur.com/EZHzKCV.gif' alt="A gif showing how you can click the link below, find your city on the list of Campsites, then click on the Facebook link for your city and join your city's Facebook group.") + img.gif-block.img-center.img-responsive(src='http://i.imgur.com/tYf8jrI.gif' alt="A gif showing how you can click the link below, find your city on the list of Campsites, then click on the Facebook link for your city and join your city's Facebook group.") .caption p.large-p Try this:  a(href='https://github.com/FreeCodeCamp/freecodecamp/wiki/List-of-Free-Code-Camp-city-based-Campsites' target='_blank') Find your city on this list - | . Click the "Join group" button to join your city's Facebook group. If your city isn't on this list,  - a(href='https://github.com/FreeCodeCamp/freecodecamp/wiki/How-to-create-a-Campsite-for-your-city' target='_blank') follow these directions to create a Facebook group for your city - | . - - .big-spacer - .thumbnail - img.gif-block.img-center.img-responsive(src='http://i.imgur.com/3AgvJQg.gif' alt="A gif showing how click the link below, find your city, and click the \"Gitter\" button to join your city's Gitter chat room") - .caption - p.large-p Try this:  - a(href='https://github.com/FreeCodeCamp/freecodecamp/wiki/List-of-Free-Code-Camp-city-based-Campsites' target='_blank') Go back to our list of Campsites  - | and click "Gitter" to join your city's chat room. + | , then click it. Click the "Join group" button to join your city's Campsite. .big-spacer .thumbnail