diff --git a/client/index.js b/client/index.js index 79c5c541c1..44bdb0a82c 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,29 @@ 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 }) => { + props.history = history; return Render( appCat, - React.createElement(Router, initialState), + React.createElement(Router, props), DOMContianer ); }) diff --git a/client/main.js b/client/main.js index 6e6ce8c696..c1578c1d6f 100644 --- a/client/main.js +++ b/client/main.js @@ -1,3 +1,32 @@ +var mapShareKey = 'map-shares'; +var lastCompleted = typeof lastCompleted !== 'undefined' ? + lastCompleted : + ''; + +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 +412,40 @@ $(document).ready(function() { } }, false); } + + + // map sharing + var alreadyShared = getMapShares(); + + 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 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' + + '&link=http%3A%2F%2Ffreecodecamp%2Ecom%2F' + + username + + '&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap'; + + setMapShare(challengeBlockName); + window.location.href = link; + }); }); function defCheck(a){ 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/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx new file mode 100644 index 0000000000..446ed957d6 --- /dev/null +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -0,0 +1,43 @@ +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(onHide) { + onHide(); + this.history.pushState(null, '/jobs/new'); + }, + + 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 a6bc6a9a9f..a4d8354b3f 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,7 +1,9 @@ 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 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: [Navigation], handleJobClick(id) { const { jobActions } = this.props; @@ -26,7 +30,7 @@ export default contain( return null; } jobActions.findJob(id); - this.transitionTo(`/jobs/${id}`); + this.history.pushState(null, `/jobs/${id}`); }, renderList(handleJobClick, jobs) { @@ -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/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 ( { + // if value exist, check if it is valid + if (props[prop].value && props[prop].type !== 'boolean') { + valid = valid && !!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; + }, {}); + + job.postedOn = new Date(); + debug('job sanitized', job); + jobActions.saveForm(job); + + this.history.pushState(null, '/jobs/new/preview'); + }, + + componentDidMount() { + const { jobActions } = this.props; + jobActions.getSavedForm(); + }, + + handleChange(name, { target: { value } }) { + const { jobActions: { handleForm } } = this.props; + handleForm({ [name]: value }); + }, + + render() { + const { + position, + locale, + description, + email, + phone, + url, + logo, + name, + highlight, + jobActions: { handleForm } + } = this.props; + const { handleChange } = this; + const labelClass = 'col-sm-offset-1 col-sm-2'; + const inputClass = 'col-sm-6'; + + return ( +
+ + + +

Create Your Job Post

+
+ +
+

Job Information

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

Company Information

+
+ handleChange('name', e) } + placeholder='Foo, INC' + type='text' + value={ name.value } + wrapperClassName={ inputClass } /> + handleChange('email', e) } + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + handleChange('phone', e) } + placeholder='555-123-1234' + type='tel' + value={ phone.value } + wrapperClassName={ inputClass } /> + handleChange('url', e) } + placeholder='http://freecatphotoapp.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> + handleChange('logo', e) } + placeholder='http://freecatphotoapp.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+

Make it stand out

+
+ handleForm({ + highlight: !!checked + }) + } + type='checkbox' /> +
+ + + + + + + + + +
+ ); + } + }) +); 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/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 4df2ae42c6..5900e6dda1 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,7 +1,9 @@ import { Actions } from 'thundercats'; +import store from 'store'; import debugFactory from 'debug'; const debug = debugFactory('freecc:jobs:actions'); +const assign = Object.assign; export default Actions({ setJobs: null, @@ -23,7 +25,7 @@ export default Actions({ // if no job found this will be null which is a op noop return foundJob ? - Object.assign({}, oldState, { currentJob: foundJob }) : + assign({}, oldState, { currentJob: foundJob }) : null; }; }, @@ -31,6 +33,31 @@ export default Actions({ getJob: null, getJobs(params) { return { params }; + }, + openModal() { + return { showModal: true }; + }, + closeModal() { + return { showModal: false }; + }, + handleForm(value) { + return { + transform(oldState) { + const { form } = oldState; + const newState = assign({}, oldState); + newState.form = assign( + {}, + form, + value + ); + return newState; + } + }; + }, + saveForm: null, + getSavedForm: null, + setForm(form) { + return { form }; } }) .refs({ displayName: 'JobActions' }) @@ -56,8 +83,22 @@ 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({}); }); }); + + jobActions.saveForm.subscribe((form) => { + store.set('newJob', form); + }); + + jobActions.getSavedForm.subscribe(() => { + const job = store.get('newJob'); + if (job && !Array.isArray(job) && typeof job === 'object') { + jobActions.setForm(job); + } + }); return jobActions; }); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index 2fdfa50207..b2f5132013 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -6,12 +6,25 @@ 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, + handleForm, + setForm + } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); - register(transformer(findJob)); register(setter(setError)); + register(setter(openModal)); + register(setter(closeModal)); + register(setter(setForm)); + + register(transformer(findJob)); + register(handleForm); }); diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index ac6b07f866..6c556c994e 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -1,5 +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 @@ -11,6 +13,12 @@ export default { childRoutes: [{ path: '/jobs', component: Jobs + }, { + path: 'jobs/new', + component: NewJob + }, { + path: 'jobs/new/preview', + component: Preview }, { path: 'jobs/:id', component: Show diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js new file mode 100644 index 0000000000..aeb0396c12 --- /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 Object.assign({}, defaults[type]); +} diff --git a/common/app/routes/index.js b/common/app/routes/index.js index 973e9f2bf0..1f14733f82 100644 --- a/common/app/routes/index.js +++ b/common/app/routes/index.js @@ -3,12 +3,8 @@ import Hikes from './Hikes'; export default { path: '/', - getChildRoutes(locationState, cb) { - setTimeout(() => { - cb(null, [ - Jobs, - Hikes - ]); - }, 0); - } + childRoutes: [ + Jobs, + Hikes + ] }; diff --git a/common/models/job.json b/common/models/job.json index a3392fee82..f77fc6defa 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": { @@ -29,6 +30,9 @@ "state": { "type": "string" }, + "url": { + "type": "string" + }, "country": { "type": "string" }, @@ -38,7 +42,7 @@ "description": { "type": "string" }, - "isApproverd": { + "isApproved": { "type": "boolean" }, "isHighlighted": { 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' ], diff --git a/package.json b/package.json index 901b54ea3a..f16e4bb9db 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,10 @@ "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", - "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", @@ -89,7 +89,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", @@ -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", + "validator": "^3.22.1", "webpack": "^1.9.12", + "xss-filters": "^1.2.6", "yui": "~3.18.1" }, "devDependencies": { 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()", diff --git a/seed/challenges/basic-javascript.json b/seed/challenges/basic-javascript.json index e0b38cc841..13c28c2021 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();", 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.')" ], diff --git a/seed/challenges/intermediate-bonfires.json b/seed/challenges/intermediate-bonfires.json index 93b21abba7..f9080c7214 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", 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": [ 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 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 11d14bceee..42fa90422b 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -131,7 +131,16 @@ 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.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. + 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 // .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.