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/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/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/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-javascript.json b/seed/challenges/basic-javascript.json index 95d0884101..fb10646035 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.", "", @@ -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 684afd44e8..62aef4687d 100644 --- a/seed/challenges/intermediate-bonfires.json +++ b/seed/challenges/intermediate-bonfires.json @@ -61,6 +61,7 @@ "diff([1, 2, 3, 5], [1, 2, 3, 4, 5]);" ], "tests": [ +<<<<<<< HEAD "assert(typeof(diff([1, 2, 3, 5], [1, 2, 3, 4, 5])) === \"object\", 'message: diff() should return an array.');", "assert.deepEqual(diff([\"diorite\", \"andesite\", \"grass\", \"dirt\", \"pink wool\", \"dead shrub\"], [\"diorite\", \"andesite\", \"grass\", \"dirt\", \"dead shrub\"]), [\"pink wool\"], 'message: [\"diorite\", \"andesite\", \"grass\", \"dirt\", \"pink wool\", \"dead shrub\"], [\"diorite\", \"andesite\", \"grass\", \"dirt\", \"dead shrub\"] should return [\"pink wool\"].');", "assert.includeMembers(diff([\"andesite\", \"grass\", \"dirt\", \"pink wool\", \"dead shrub\"], [\"diorite\", \"andesite\", \"grass\", \"dirt\", \"dead shrub\"]), [\"diorite\", \"pink wool\"], 'message: [\"andesite\", \"grass\", \"dirt\", \"pink wool\", \"dead shrub\"], [\"diorite\", \"andesite\", \"grass\", \"dirt\", \"dead shrub\"] should return [\"diorite\", \"pink wool\"].');", @@ -68,6 +69,15 @@ "assert.deepEqual(diff([1, 2, 3, 5], [1, 2, 3, 4, 5]), [4], 'message: [1, 2, 3, 5], [1, 2, 3, 4, 5] should return [4].');", "assert.includeMembers(diff([1, \"calf\", 3, \"piglet\"], [1, \"calf\", 3, 4]), [\"piglet\", 4], 'message: [1, \"calf\", 3, \"piglet\"], [1, \"calf\", 3, 4] should return [\"piglet\", 4].');", "assert.deepEqual(diff([], [\"snuffleupagus\", \"cookie monster\", \"elmo\"]), [\"snuffleupagus\", \"cookie monster\", \"elmo\"], 'message: [], [\"snuffleupagus\", \"cookie monster\", \"elmo\"] should return [\"snuffleupagus\", \"cookie monster\", \"elmo\"].');" +======= + "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');", + "assert.deepEqual(diff([1, 2, 3, 5], [1, 2, 3, 4, 5]), [4], 'arrays with numbers');", + "assert.includeMembers(diff([1, 'calf', 3, 'piglet'], [1, 'calf', 3, 4]), ['piglet', 4], 'arrays with numbers and strings');", + "assert.deepEqual(diff([], ['snuffleupagus', 'cookie monster', 'elmo']), ['snuffleupagus', 'cookie monster', 'elmo'], 'empty array');" +>>>>>>> staging ], "MDNlinks": [ "Comparison Operators", @@ -93,11 +103,19 @@ "id": "a7f4d8f2483413a6ce226cac", "title": "Roman Numeral Converter", "tests": [ +<<<<<<< HEAD "assert.deepEqual(convert(12), \"XII\", 'message: convert(12) should return \"XII\".');", "assert.deepEqual(convert(5), \"V\", 'message: convert(5) should return \"V\".');", "assert.deepEqual(convert(9), \"IX\", 'message: convert(9) should return \"IX\".');", "assert.deepEqual(convert(29), \"XXIX\", 'message: convert(29) should return \"XXIX\".');", "assert.deepEqual(convert(16), \"XVI\", 'message: convert(16) should return \"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\"');" +>>>>>>> staging ], "difficulty": "2.02", "description": [ @@ -150,9 +168,16 @@ "where([{ first: \"Romeo\", last: \"Montague\" }, { first: \"Mercutio\", last: null }, { first: \"Tybalt\", last: \"Capulet\" }], { last: \"Capulet\" });" ], "tests": [ +<<<<<<< HEAD "assert.deepEqual(where([{ first: \"Romeo\", last: \"Montague\" }, { first: \"Mercutio\", last: null }, { first: \"Tybalt\", last: \"Capulet\" }], { last: \"Capulet\" }), [{ first: \"Tybalt\", last: \"Capulet\" }], 'message: where() 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 }], 'message: where([{ \"a\": 1 }, { \"a\": 1 }, { \"a\": 1, \"b\": 2 }], { \"a\": 1 }) should return [{ \"a\": 1 }, { \"a\": 1 }, { \"a\": 1, \"b\": 2 }].');", "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 }], 'message: where([{ \"a\": 1, \"b\": 2 }, { \"a\": 1 }, { \"a\": 1, \"b\": 2, \"c\": 2 }], { \"a\": 1, \"b\": 2 }) should return [{ \"a\": 1, \"b\": 2 }, { \"a\": 1, \"b\": 2, \"c\": 2 }].');" +======= + "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 }, { 'b': 10 }, { 'a': 5, 'b': 10 }], { 'a': 5, 'b': 10 }), [{ 'a': 5, 'b': 10 }], 'should return a single object in array');" +>>>>>>> staging ], "MDNlinks": [ "Global Object", @@ -176,11 +201,19 @@ "id": "a0b5010f579e69b815e7c5d6", "title": "Search and Replace", "tests": [ +<<<<<<< HEAD "assert.deepEqual(myReplace(\"Let us go to the store\", \"store\", \"mall\"), \"Let us go to the mall\", 'message: myReplace(\"Let us go to the store\", \"store\", \"mall\") should return \"Let us go to the mall\".');", "assert.deepEqual(myReplace(\"He is Sleeping on the couch\", \"Sleeping\", \"sitting\"), \"He is Sitting on the couch\", 'message: myReplace(\"He is Sleeping on the couch\", \"Sleeping\", \"sitting\") should return \"He is Sitting on the couch\".');", "assert.deepEqual(myReplace(\"This has a spellngi error\", \"spellngi\", \"spelling\"), \"This has a spelling error\", 'message: myReplace(\"This has a spellngi error\", \"spellingi\", \"spelling\") should return \"This has a spelling error\".');", "assert.deepEqual(myReplace(\"His name is Tom\", \"Tom\", \"john\"), \"His name is John\", 'message: myReplace(\"His name is Tom\", \"Tom\", \"john\") should return \"His name is John\".');", "assert.deepEqual(myReplace(\"Let us get back to more Coding\", \"Coding\", \"bonfires\"), \"Let us get back to more Bonfires\", 'message: myReplace(\"Let us get back to more Coding\", \"Coding\", \"bonfires\") should return \"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\"');" +>>>>>>> staging ], "difficulty": "2.035", "description": [ @@ -220,11 +253,19 @@ "id": "aa7697ea2477d1316795783b", "title": "Pig Latin", "tests": [ +<<<<<<< HEAD "assert.deepEqual(translate(\"california\"), \"aliforniacay\", 'message: translate(\"california\") should return \"aliforniacay\".');", "assert.deepEqual(translate(\"paragraphs\"), \"aragraphspay\", 'message: translate(\"paragraphs\") should return \"aragraphspay\".');", "assert.deepEqual(translate(\"glove\"), \"oveglay\", 'message: translate(\"glove\") should return \"oveglay\".');", "assert.deepEqual(translate(\"algorithm\"), \"algorithmway\", 'message: translate(\"algorithm\") should return \"algorithmway\".');", "assert.deepEqual(translate(\"eight\"), \"eightway\", 'message: translate(\"eight\") should return \"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\"');" +>>>>>>> staging ], "difficulty": "2.04", "description": [ @@ -318,10 +359,17 @@ "fearNotLetter(\"abce\");" ], "tests": [ +<<<<<<< HEAD "assert.deepEqual(fearNotLetter(\"abce\"), \"d\", 'message: fearNotLetter(\"abce\") should return \"d\".');", "assert.deepEqual(fearNotLetter(\"abcdefghjklmno\"), \"i\", 'message: fearNotLetter(\"abcdefghjklmno\") should return \"i\".');", "assert.isUndefined(fearNotLetter(\"bcd\"), 'message: fearNotLetter(\"bcd\") should return undefined.');", "assert.isUndefined(fearNotLetter(\"yz\"), 'message: fearNotLetter(\"yz\") should return 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');" +>>>>>>> staging ], "MDNlinks": [ "String.charCodeAt()", @@ -524,12 +572,21 @@ "sumFibs(4);" ], "tests": [ +<<<<<<< HEAD "assert(typeof(sumFibs(1)) === \"number\", 'message: sumFibs() should return a number.');", "assert.deepEqual(sumFibs(1000), 1785, 'message: sumFibs(1000) should return 1785.');", "assert.deepEqual(sumFibs(4000000), 4613732, 'message: sumFibs(4000000) should return 4613732.');", "assert.deepEqual(sumFibs(4), 5, 'message: sumFibs(4) should return 5.');", "assert.deepEqual(sumFibs(75024), 60696, 'message: sumFibs(75024) should return 60696.');", "assert.deepEqual(sumFibs(75025), 135721, 'message: sumFibs(75025) should return 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');" +>>>>>>> staging ], "MDNlinks": [ "Remainder" @@ -565,9 +622,15 @@ "sumPrimes(10);" ], "tests": [ +<<<<<<< HEAD "assert.deepEqual(typeof(sumPrimes(10)), \"number\", 'message: sumPrimes() should return a number.');", "assert.deepEqual(sumPrimes(10), 17, 'message: sumPrimes(10) should return 17.');", "assert.deepEqual(sumPrimes(977), 73156, 'message: sumPrimes(977) should return 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');" +>>>>>>> staging ], "MDNlinks": [ "For Loops", @@ -605,10 +668,17 @@ "smallestCommons([1,5]);" ], "tests": [ +<<<<<<< HEAD "assert.deepEqual(typeof(smallestCommons([1, 5])), \"number\", 'message: smallestCommons() should return a number.');", "assert.deepEqual(smallestCommons([1, 5]), 60, 'message: smallestCommons([1, 5]) should return 60.');", "assert.deepEqual(smallestCommons([5, 1]), 60, 'message: smallestCommons([5, 1]) should return 60.');", "assert.deepEqual(smallestCommons([1, 13]), 360360, 'message: smallestCommons([1, 13]) should return 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');" +>>>>>>> staging ], "MDNlinks": [ "Smallest Common Multiple" @@ -679,10 +749,17 @@ "drop([1, 2, 3], function(n) {return n < 3; });" ], "tests": [ +<<<<<<< HEAD "assert.deepEqual(drop([1, 2, 3, 4], function(n) {return n>= 3;}), [3, 4], 'message: 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], 'message: 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;}), [], 'message: 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], 'message: drop([1, 2, 3, 7, 4], function(n) {return n>= 3}) should return [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]');" +>>>>>>> staging ], "MDNlinks": [ "Arguments object", @@ -756,8 +833,13 @@ "binaryAgent(\"01000001 01110010 01100101 01101110 00100111 01110100 00100000 01100010 01101111 01101110 01100110 01101001 01110010 01100101 01110011 00100000 01100110 01110101 01101110 00100001 00111111\");" ], "tests": [ +<<<<<<< HEAD "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!?\", 'message: binaryAgent(\"01000001 01110010 01100101 01101110 00100111 01110100 00100000 01100010 01101111 01101110 01100110 01101001 01110010 01100101 01110011 00100000 01100110 01110101 01101110 00100001 00111111\") 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!\", 'message: binaryAgent(\"01001001 00100000 01101100 01101111 01110110 01100101 00100000 01000110 01110010 01100101 01100101 01000011 01101111 01100100 01100101 01000011 01100001 01101101 01110000 00100001\" should return \"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!\"');" +>>>>>>> staging ], "MDNlinks": [ "String.charCodeAt()", @@ -837,11 +919,19 @@ "add(2,3);" ], "tests": [ +<<<<<<< HEAD "assert.deepEqual(add(2, 3), 5, 'message: add(2, 3) should return 5.');", "assert.deepEqual(add(2)(3), 5, 'message: add(2)(3) should return 5.');", "assert.isUndefined(add(\"http://bit.ly/IqT6zt\"), 'message: add(\"http://bit.ly/IqT6zt\") should return undefined.');", "assert.isUndefined(add(2, \"3\"), 'message: add(2, \"3\") should return undefined.');", "assert.isUndefined(add(2)([3]), 'message: add(2)([3]) should return 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');" +>>>>>>> staging ], "MDNlinks": [ "Global Function Object", 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": [ 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/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/boot/user.js b/server/boot/user.js index 33a80c5c62..9c6450db9c 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', vote2); // 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/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(); } diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 05d591b010..42fa90422b 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -17,7 +17,36 @@ 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 + 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 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") + 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 + h3.text-center "Minified JavaScript Logo" + a.button.btn.btn-block.btn-primary(href='/vote1') Vote for this Design + .col-xs-6 + 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 h3 800 Hours of Practice: @@ -102,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/showBonfire.jade b/server/views/coursewares/showBonfire.jade index 084fd03d4b..e64424e1ac 100644 --- a/server/views/coursewares/showBonfire.jade +++ b/server/views/coursewares/showBonfire.jade @@ -55,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/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. 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 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)};