From e4e87fd1a114b25eeaa0481669c6a4cbbeb4cac3 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 11 Oct 2015 20:25:09 -0700 Subject: [PATCH 01/55] Show N/A when field is empty --- common/app/routes/Jobs/components/ShowJob.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index 1a048a3fff..80b0242a2e 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -2,6 +2,10 @@ import React, { PropTypes } from 'react'; import { Row, Thumbnail, Panel, Well } from 'react-bootstrap'; import moment from 'moment'; +const defaultImage = + 'https://pbs.twimg.com/' + + 'profile_images/562385977390272512/AK29YaTf_400x400.png'; + const thumbnailStyle = { backgroundColor: 'white', maxHeight: '100px', @@ -47,12 +51,13 @@ export default React.createClass({ - Position: { position } - Location: { city }, { state } + Position: { position || 'N/A' } +
+ Location: { city || '' }, { state || 'N/A' }
Contact: { email || phone || 'N/A' }
From 1f16d6f5d9cc28427eec50651aaf80ff710f87bd Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 11 Oct 2015 21:11:27 -0700 Subject: [PATCH 02/55] Add required fields to job form --- common/app/routes/Jobs/components/NewJob.jsx | 18 +++++++++++++----- common/app/routes/Jobs/components/ShowJob.jsx | 3 ++- common/app/routes/Jobs/flux/Actions.js | 1 + 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 4c369411f0..ed4aca7276 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -58,6 +58,10 @@ function isValidPhone(data) { return isMobilePhone(data, 'en-US'); } +function makeRequired(validator) { + return (val) => !!val && validator(val); +} + export default contain({ actions: 'jobActions', store: 'jobsStore', @@ -74,14 +78,14 @@ export default contain({ highlight } = form; return { - position: formatValue(position, isAscii), - locale: formatValue(locale, isAscii), - description: formatValue(description, isAscii), - email: formatValue(email, isEmail), + position: formatValue(position, makeRequired(isAscii)), + locale: formatValue(locale, makeRequired(isAscii)), + description: formatValue(description, makeRequired(isAscii)), + email: formatValue(email, makeRequired(isEmail)), phone: formatValue(phone, isValidPhone), url: formatValue(url, isValidURL), logo: formatValue(logo, isValidURL), - name: formatValue(name, isAscii), + name: formatValue(name, makeRequired(isAscii)), highlight: formatValue(highlight, null, 'bool') }; }, @@ -209,6 +213,7 @@ export default contain({ labelClassName={ labelClass } onChange={ (e) => handleChange('position', e) } placeholder='Position' + required={ true } type='text' value={ position.value } wrapperClassName={ inputClass } /> @@ -218,6 +223,7 @@ export default contain({ labelClassName={ labelClass } onChange={ (e) => handleChange('locale', e) } placeholder='Location' + required={ true } type='text' value={ locale.value } wrapperClassName={ inputClass } /> @@ -227,6 +233,7 @@ export default contain({ labelClassName={ labelClass } onChange={ (e) => handleChange('description', e) } placeholder='Description' + required={ true } rows='10' type='textarea' value={ description.value } @@ -250,6 +257,7 @@ export default contain({ labelClassName={ labelClass } onChange={ (e) => handleChange('email', e) } placeholder='Email' + required={ true } type='email' value={ email.value } wrapperClassName={ inputClass } /> diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index 80b0242a2e..b89d2b681a 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -40,6 +40,7 @@ export default React.createClass({ city, company, state, + locale, email, phone, postedOn, @@ -57,7 +58,7 @@ export default React.createClass({ Position: { position || 'N/A' }
- Location: { city || '' }, { state || 'N/A' } + Location: { locale ? locale : `${city}, ${state}` }
Contact: { email || phone || 'N/A' }
diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 5900e6dda1..db7d84d8c9 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -57,6 +57,7 @@ export default Actions({ saveForm: null, getSavedForm: null, setForm(form) { + form.locale = form.location; return { form }; } }) From 4577a007f2b32d643fdbb9884cc5f8ef26661154 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 12 Oct 2015 17:12:38 -0700 Subject: [PATCH 03/55] Add goback and checkout buttons to preview job --- common/app/routes/Jobs/components/NewJob.jsx | 2 +- common/app/routes/Jobs/components/Preview.jsx | 38 ++++++++++++++++++- common/app/routes/Jobs/flux/Actions.js | 1 - common/models/job.json | 7 +++- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index ed4aca7276..3c2c16bf37 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -143,7 +143,7 @@ export default contain({ // sanitize user output const jobValues = { position: inHTMLData(position.value), - location: inHTMLData(locale.value), + locale: inHTMLData(locale.value), description: inHTMLData(description.value), email: inHTMLData(email.value), phone: inHTMLData(phone.value), diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 5b6081be5c..7f7728c847 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,4 +1,6 @@ -// import React, { PropTypes } from 'react'; +import React, { PropTypes } from 'react'; +import { History } from 'react-router'; +import { Well, Button, Row } from 'react-bootstrap'; import { contain } from 'thundercats-react'; import ShowJob from './ShowJob.jsx'; @@ -10,5 +12,37 @@ export default contain( return { job }; } }, - ShowJob + React.createClass({ + displayName: 'Preview', + + mixins: [History], + + propTypes: { + job: PropTypes.object + }, + + render() { + const { job } = this.props; + const { history } = this; + return ( +
+ + + + + + + +
+ ); + } + }) ); diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index db7d84d8c9..5900e6dda1 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -57,7 +57,6 @@ export default Actions({ saveForm: null, getSavedForm: null, setForm(form) { - form.locale = form.location; return { form }; } }) diff --git a/common/models/job.json b/common/models/job.json index f77fc6defa..49866b0a58 100644 --- a/common/models/job.json +++ b/common/models/job.json @@ -36,8 +36,13 @@ "country": { "type": "string" }, + "locale": { + "type": "string", + "description": "format: city, state" + }, "location": { - "type": "geopoint" + "type": "geopoint", + "description": "location in lat, long" }, "description": { "type": "string" From da26f19cde67b7bc098785ddfdcb37d3058459f6 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 13 Oct 2015 23:05:31 -0700 Subject: [PATCH 04/55] Adds paypal button and completes the number of views --- client/index.js | 2 +- common/app/Cat.js | 6 +- common/app/flux/Actions.js | 14 +++- .../app/routes/Jobs/components/GoToPayPal.jsx | 71 +++++++++++++++++++ .../Jobs/components/NewJobCompleted.jsx | 19 +++++ common/app/routes/Jobs/components/Preview.jsx | 25 ++++--- common/app/routes/Jobs/flux/Actions.js | 15 +++- common/app/routes/Jobs/index.js | 8 +++ server/services/job.js | 10 ++- 9 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 common/app/routes/Jobs/components/GoToPayPal.jsx create mode 100644 common/app/routes/Jobs/components/NewJobCompleted.jsx diff --git a/client/index.js b/client/index.js index 44bdb0a82c..73b241041f 100644 --- a/client/index.js +++ b/client/index.js @@ -27,7 +27,7 @@ app$({ history, location: appLocation }) .flatMap( ({ AppCat }) => { // instantiate the cat with service - const appCat = AppCat(null, services); + const appCat = AppCat(null, services, history); // hydrate the stores return hydrate(appCat, catState) .map(() => appCat); diff --git a/common/app/Cat.js b/common/app/Cat.js index 383409b24c..6027fa9f2d 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -5,13 +5,13 @@ import { HikesActions, HikesStore } from './routes/Hikes/flux'; import { JobActions, JobsStore} from './routes/Jobs/flux'; export default Cat() - .init(({ instance: cat, args: [services] }) => { - cat.register(AppActions, null, services); + .init(({ instance: cat, args: [services, history] }) => { + cat.register(AppActions, null, services, history); cat.register(AppStore, null, cat); cat.register(HikesActions, null, services); cat.register(HikesStore, null, cat); - cat.register(JobActions, null, services); + cat.register(JobActions, null, cat, services); cat.register(JobsStore, null, cat); }); diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 2cf852f483..2c4545afaf 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -16,10 +16,20 @@ export default Actions({ }; }, - getUser: null + getUser: null, + goTo: null, + goBack: null }) .refs({ displayName: 'AppActions' }) - .init(({ instance: appActions, args: [services] }) => { + .init(({ instance: appActions, args: [services, history] }) => { + appActions.goTo.subscribe((url) => { + history.pushState(null, url); + }); + + appActions.goBack.subscribe(() => { + history.goBack(); + }); + appActions.getUser.subscribe(({ isPrimed }) => { if (isPrimed) { debug('isPrimed'); diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx new file mode 100644 index 0000000000..cedeeb5674 --- /dev/null +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -0,0 +1,71 @@ +import React, { PropTypes } from 'react'; +import { Col, Well, Row } from 'react-bootstrap'; +import { contain } from 'thundercats-react'; + +export default contain( + { + store: 'JobsStore', + actions: 'JobActions', + map({ job: { id } = {} }) { + return { id }; + } + }, + React.createClass({ + displayName: 'GoToPayPal', + + propTypes: { + id: PropTypes.string + }, + + render() { + const { id } = this.props; + return ( +
+ + + +
+ + + + + +
+
+ +
+
+ ); + } + }) +); diff --git a/common/app/routes/Jobs/components/NewJobCompleted.jsx b/common/app/routes/Jobs/components/NewJobCompleted.jsx new file mode 100644 index 0000000000..1a78e87eaa --- /dev/null +++ b/common/app/routes/Jobs/components/NewJobCompleted.jsx @@ -0,0 +1,19 @@ +import React, { PropTypes } from 'react'; +import { Well, Row } from 'react-bootstrap'; + +export default React.createClass({ + displayName: 'NewJobCompleted', + propTypes: { + }, + render() { + return ( +
+ + + Congrats! + + +
+ ); + } +}); diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 7f7728c847..752c10ed4b 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,5 +1,4 @@ import React, { PropTypes } from 'react'; -import { History } from 'react-router'; import { Well, Button, Row } from 'react-bootstrap'; import { contain } from 'thundercats-react'; import ShowJob from './ShowJob.jsx'; @@ -7,7 +6,10 @@ import ShowJob from './ShowJob.jsx'; export default contain( { store: 'JobsStore', - actions: 'JobActions', + actions: [ + 'appActions', + 'jobActions' + ], map({ form: job = {} }) { return { job }; } @@ -15,15 +17,14 @@ export default contain( React.createClass({ displayName: 'Preview', - mixins: [History], - propTypes: { - job: PropTypes.object + appActions: PropTypes.object, + job: PropTypes.object, + jobActions: PropTypes.object }, render() { - const { job } = this.props; - const { history } = this; + const { appActions, job, jobActions } = this.props; return (
@@ -31,12 +32,18 @@ export default contain( diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 5900e6dda1..d37f16a7f5 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -31,6 +31,7 @@ export default Actions({ }, setError: null, getJob: null, + saveJobToDb: null, getJobs(params) { return { params }; }, @@ -61,7 +62,7 @@ export default Actions({ } }) .refs({ displayName: 'JobActions' }) - .init(({ instance: jobActions, args: [services] }) => { + .init(({ instance: jobActions, args: [cat, services] }) => { jobActions.getJobs.subscribe(() => { services.read('jobs', null, null, (err, jobs) => { if (err) { @@ -100,5 +101,17 @@ export default Actions({ jobActions.setForm(job); } }); + + jobActions.saveJobToDb.subscribe(({ goTo, job }) => { + const appActions = cat.getActions('appActions'); + services.create('jobs', { job }, null, (err, job) => { + if (err) { + debug('job services experienced an issue', err); + return jobActions.setError(err); + } + jobActions.setJobs({ job }); + appActions.goTo(goTo); + }); + }); return jobActions; }); diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index 6c556c994e..9d8ee8c2dc 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -2,6 +2,8 @@ import Jobs from './components/Jobs.jsx'; import NewJob from './components/NewJob.jsx'; import Show from './components/Show.jsx'; import Preview from './components/Preview.jsx'; +import GoToPayPal from './components/GoToPayPal.jsx'; +import NewJobCompleted from './components/NewJobCompleted.jsx'; /* * index: /jobs list jobs @@ -19,6 +21,12 @@ export default { }, { path: 'jobs/new/preview', component: Preview + }, { + path: 'jobs/new/check-out', + component: GoToPayPal + }, { + path: 'jobs/new/completed', + component: NewJobCompleted }, { path: 'jobs/:id', component: Show diff --git a/server/services/job.js b/server/services/job.js index fe54d0064c..0bbc46db34 100644 --- a/server/services/job.js +++ b/server/services/job.js @@ -3,7 +3,15 @@ export default function getJobServices(app) { return { name: 'jobs', - read: (req, resource, params, config, cb) => { + create(req, resource, { job } = {}, body, config, cb) { + if (!job) { + return cb(new Error('job creation should get a job object')); + } + Job.create(job, (err, savedJob) => { + cb(err, savedJob); + }); + }, + read(req, resource, params, config, cb) { const id = params ? params.id : null; if (id) { return Job.findById(id, cb); From a65384e69880505398fe0cf0f67080c027279305 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 20:03:07 -0700 Subject: [PATCH 05/55] Make list an actual list instead of accordion --- client/less/jobs.less | 7 ++ client/less/main.less | 3 +- .../app/routes/Jobs/components/GoToPayPal.jsx | 12 +-- common/app/routes/Jobs/components/Jobs.jsx | 36 ++++---- common/app/routes/Jobs/components/List.jsx | 88 +++++++------------ common/models/job.json | 13 ++- 6 files changed, 74 insertions(+), 85 deletions(-) create mode 100644 client/less/jobs.less diff --git a/client/less/jobs.less b/client/less/jobs.less new file mode 100644 index 0000000000..428855e97e --- /dev/null +++ b/client/less/jobs.less @@ -0,0 +1,7 @@ +.jobs-list-highlight { + background-color: #ffc +} + +a.jobs-list-highlight:hover { + background-color: #ffc +} diff --git a/client/less/main.less b/client/less/main.less index aa70867894..b24a4a35b7 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -955,11 +955,12 @@ code { margin: 0!important; } -// gitter chat .gitter-chat-embed { z-index: 20000 !important; } +@import "jobs.less"; + //uncomment this to see the dimensions of all elements outlined in red //* { // border-color: red; diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index cedeeb5674..3314b45a82 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Col, Well, Row } from 'react-bootstrap'; +import { Col, Panel, Row } from 'react-bootstrap'; import { contain } from 'thundercats-react'; export default contain( @@ -23,12 +23,12 @@ export default contain(
- + xs={ 12 }> +
-
+
diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index a4d8354b3f..ab5834c706 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,7 +1,7 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; import { History } from 'react-router'; -import { Button, Jumbotron, Row } from 'react-bootstrap'; +import { Button, Panel, Row } from 'react-bootstrap'; import CreateJobModal from './CreateJobModal.jsx'; import ListJobs from './List.jsx'; @@ -60,31 +60,29 @@ export default contain( } = this.props; return ( -
+ +

Free Code Camps' Job Board

- -

Free Code Camps' Job Board

-

- Need to find the best junior developers? - Want to find dedicated developers eager to join your company? - Sign up now to post your job! -

- -
+

+ Need to find the best junior developers? + Want to find dedicated developers eager to join your company? + Sign up now to post your job! +

+
- { this.renderChild(children, jobs) || - this.renderList(this.handleJobClick, jobs) } + { 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 2457bcb7f0..d416497721 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { PanelGroup, Thumbnail, Panel, Well } from 'react-bootstrap'; +import { ListGroup, ListGroupItem } from 'react-bootstrap'; import moment from 'moment'; export default React.createClass({ @@ -10,64 +10,40 @@ export default React.createClass({ jobs: PropTypes.array }, - renderJobs(handleClick, jobs =[]) { - const thumbnailStyle = { - backgroundColor: 'white', - maxHeight: '100px', - maxWidth: '100px' - }; - - return jobs.map(( - { + renderJobs(handleClick, jobs = []) { + return jobs + .filter(({ isPaid, isApproved, isFilled }) => { + return isPaid && isApproved && !isFilled; + }) + .map(({ id, company, position, isHighlighted, - description, - logo, - city, - state, - email, - phone, postedOn - }, - index - ) => { - const header = ( -
-

{ company }

-
- { position } -
-
- ); - return ( - - - - - Position: { position } - Location: { city }, { state } -
- Contact: { email || phone || 'N/A' } -
- Posted On: { moment(postedOn).format('MMMM Do, YYYY') } -
-

handleClick(id) }>{ description }

-
-
- ); - }); + }) => { + return ( + handleClick(id) }> +
+

+ { company } + {' '} + + - { position } + + {' '} +

+

+ { moment(new Date(postedOn)).format('MMM Do') } +

+
+
+ ); + }); }, render() { @@ -77,9 +53,9 @@ export default React.createClass({ } = this.props; return ( - + { this.renderJobs(handleClick, jobs) } - + ); } }); diff --git a/common/models/job.json b/common/models/job.json index 49866b0a58..673a5e0477 100644 --- a/common/models/job.json +++ b/common/models/job.json @@ -48,13 +48,20 @@ "type": "string" }, "isApproved": { - "type": "boolean" + "type": "boolean", + "default": false }, "isHighlighted": { - "type": "boolean" + "type": "boolean", + "default": false }, "isPaid": { - "type": "boolean" + "type": "boolean", + "default": false + }, + "isFilled": { + "type": "boolean", + "default": false }, "postedOn": { "type": "date", From 979583e0c9293fd0d767b21e7a244d0bc897cae0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 21:10:06 -0700 Subject: [PATCH 06/55] Add styling to jobs list --- common/app/routes/Jobs/components/Jobs.jsx | 52 +++++++++++++--------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index ab5834c706..fec91fc342 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,7 +1,7 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; import { History } from 'react-router'; -import { Button, Panel, Row } from 'react-bootstrap'; +import { Button, Panel, Row, Col } from 'react-bootstrap'; import CreateJobModal from './CreateJobModal.jsx'; import ListJobs from './List.jsx'; @@ -61,27 +61,39 @@ export default contain( return ( -

Free Code Camps' Job Board

-

- Need to find the best junior developers? - Want to find dedicated developers eager to join your company? - Sign up now to post your job! -

- + +

Free Code Camps' Job Board

+ + +

+ Need to find the best junior developers? + Post your job today! +

+ +
+ + + + { this.renderChild(children, jobs) || + this.renderList(this.handleJobClick, jobs) } + + + - - { this.renderChild(children, jobs) || - this.renderList(this.handleJobClick, jobs) } - - ); } From 1ed576bee11e88d01dfdb40b146cdee8440b741d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 22:09:12 -0700 Subject: [PATCH 07/55] Style show job page --- common/app/routes/Jobs/components/ShowJob.jsx | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index b89d2b681a..b1d8faad5a 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Row, Thumbnail, Panel, Well } from 'react-bootstrap'; +import { Row, Col, Thumbnail, Panel } from 'react-bootstrap'; import moment from 'moment'; const defaultImage = @@ -50,22 +50,49 @@ export default React.createClass({ return (
- - + - Position: { position || 'N/A' } -
- Location: { locale ? locale : `${city}, ${state}` } -
- Contact: { email || phone || 'N/A' } -
- Posted On: { moment(postedOn).format('MMMM Do, YYYY') } + +

+ { company } +

+
+
+ + + + + + + Position: { position || 'N/A' } +
+ Location: { locale ? locale : `${city}, ${state}` } +
+ Contact: { email || phone || 'N/A' } +
+ Posted On: { moment(postedOn).format('MMMM Do, YYYY') } + +
+
+ + +

{ description }

+ +
-

{ description }

- +
); From 6512170e9b6e38306d4ce03b77232ad0f9a6b354 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 22:24:05 -0700 Subject: [PATCH 08/55] Add styling to preview jobs Add cursor pointer --- client/less/jobs.less | 5 +++ common/app/routes/Jobs/components/List.jsx | 9 +++- common/app/routes/Jobs/components/NewJob.jsx | 10 +++-- common/app/routes/Jobs/components/Preview.jsx | 44 +++++++++++-------- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/client/less/jobs.less b/client/less/jobs.less index 428855e97e..cb1175f536 100644 --- a/client/less/jobs.less +++ b/client/less/jobs.less @@ -5,3 +5,8 @@ a.jobs-list-highlight:hover { background-color: #ffc } + +.jobs-list { + cursor: pointer; + cursor: hand; +} diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index d416497721..2ba49a064f 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import classnames from 'classnames'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import moment from 'moment'; @@ -22,9 +23,15 @@ export default React.createClass({ isHighlighted, postedOn }) => { + + const className = classnames({ + 'jobs-list': true, + 'jobs-list-highlight': isHighlighted + }); + return ( handleClick(id) }>

diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 3c2c16bf37..440fb7df53 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -14,7 +14,7 @@ import { Col, Input, Row, - Well + Panel } from 'react-bootstrap'; import { @@ -197,8 +197,10 @@ export default contain({ return (
- - + +

Create Your Job Post

-
+
diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 752c10ed4b..a6249e4b2a 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Well, Button, Row } from 'react-bootstrap'; +import { Panel, Button, Row, Col } from 'react-bootstrap'; import { contain } from 'thundercats-react'; import ShowJob from './ShowJob.jsx'; @@ -29,24 +29,30 @@ export default contain(
- - - - + + + + + +
); From defd0561f1e551dc9246d364cc55fe4c552ee0db Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 22:30:07 -0700 Subject: [PATCH 09/55] Change name to company name in form --- common/app/routes/Jobs/components/NewJob.jsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 440fb7df53..611bd49f85 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -34,7 +34,7 @@ const checkValidity = [ 'phone', 'url', 'logo', - 'name', + 'company', 'highlight' ]; @@ -74,7 +74,7 @@ export default contain({ phone, url, logo, - name, + company, highlight } = form; return { @@ -85,7 +85,7 @@ export default contain({ phone: formatValue(phone, isValidPhone), url: formatValue(url, isValidURL), logo: formatValue(logo, isValidURL), - name: formatValue(name, makeRequired(isAscii)), + company: formatValue(company, makeRequired(isAscii)), highlight: formatValue(highlight, null, 'bool') }; }, @@ -105,7 +105,7 @@ export default contain({ phone: PropTypes.object, url: PropTypes.object, logo: PropTypes.object, - name: PropTypes.object, + company: PropTypes.object, highlight: PropTypes.object }, @@ -135,7 +135,7 @@ export default contain({ phone, url, logo, - name, + company, highlight, jobActions } = this.props; @@ -149,7 +149,7 @@ export default contain({ phone: inHTMLData(phone.value), url: uriInSingleQuotedAttr(url.value), logo: uriInSingleQuotedAttr(logo.value), - name: inHTMLData(name.value), + company: inHTMLData(company.value), highlight: !!highlight.value }; @@ -186,7 +186,7 @@ export default contain({ phone, url, logo, - name, + company, highlight, jobActions: { handleForm } } = this.props; @@ -245,13 +245,13 @@ export default contain({

Company Information

handleChange('name', e) } + onChange={ (e) => handleChange('company', e) } placeholder='Foo, INC' type='text' - value={ name.value } + value={ company.value } wrapperClassName={ inputClass } /> Date: Thu, 15 Oct 2015 23:06:16 -0700 Subject: [PATCH 10/55] Add styling to GoToPaypal page --- .../app/routes/Jobs/components/GoToPayPal.jsx | 98 ++++++++++++------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 3314b45a82..a29ecbe976 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -1,20 +1,21 @@ import React, { PropTypes } from 'react'; -import { Col, Panel, Row } from 'react-bootstrap'; +import { Button, Col, Panel, Row } from 'react-bootstrap'; import { contain } from 'thundercats-react'; export default contain( { store: 'JobsStore', actions: 'JobActions', - map({ job: { id } = {} }) { - return { id }; + map({ job: { id, isHighlighted } = {} }) { + return { id, isHighlighted }; } }, React.createClass({ displayName: 'GoToPayPal', propTypes: { - id: PropTypes.string + id: PropTypes.string, + isHighlighted: PropTypes.bool }, render() { @@ -23,44 +24,65 @@ export default contain(
-
- - - - - -
+ + +

+ One more step +

+
+ You're Awesome! just one more step to go. + Clicking on the link below will redirect to paypal. + + +
+ + +
+ + + + +
+ An array of credit cards + + + +
From c3776175a33098e8524cf6d6e2070b1dea469d6e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 23:33:26 -0700 Subject: [PATCH 11/55] Add styling to job completed view --- .../Jobs/components/NewJobCompleted.jsx | 37 +++++++++++++++---- package.json | 1 + 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJobCompleted.jsx b/common/app/routes/Jobs/components/NewJobCompleted.jsx index 1a78e87eaa..5d9e5785c1 100644 --- a/common/app/routes/Jobs/components/NewJobCompleted.jsx +++ b/common/app/routes/Jobs/components/NewJobCompleted.jsx @@ -1,18 +1,41 @@ -import React, { PropTypes } from 'react'; -import { Well, Row } from 'react-bootstrap'; +import React from 'react'; +import { LinkContainer } from 'react-router-bootstrap'; +import { Button, Panel, Col, Row } from 'react-bootstrap'; export default React.createClass({ displayName: 'NewJobCompleted', + propTypes: { }, + render() { return (
- - - Congrats! - - + + +

+ Job under review +

+
+ + + Congrats! Your job has been posted and is under review. + Once we review you job post we will publish it and you will receive + an email from us with a link to the listing. + + +
+ + + +
); } diff --git a/package.json b/package.json index 1c6f77559a..c4777546c9 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "react-bootstrap": "~0.23.7", "react-motion": "~0.1.0", "react-router": "https://github.com/BerkeleyTrue/react-router.git#freecodecamp", + "react-router-bootstrap": "^0.19.2", "react-vimeo": "^0.0.3", "request": "~2.53.0", "rev-del": "^1.0.5", From ede8da38ba2ce85fb54027e4d068f73a420c5b62 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 15 Oct 2015 23:43:24 -0700 Subject: [PATCH 12/55] Filter unpaid, unapproved and filled jobs Fix missing key in react array --- common/app/routes/Jobs/components/List.jsx | 1 + server/services/job.js | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index 2ba49a064f..368a376294 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -32,6 +32,7 @@ export default React.createClass({ return ( handleClick(id) }>

diff --git a/server/services/job.js b/server/services/job.js index 0bbc46db34..a3807a6f28 100644 --- a/server/services/job.js +++ b/server/services/job.js @@ -1,3 +1,11 @@ +const whereFilt = { + where: { + isFilled: false, + isPaid: true, + isApproved: true + } +}; + export default function getJobServices(app) { const { Job } = app.models; @@ -16,7 +24,7 @@ export default function getJobServices(app) { if (id) { return Job.findById(id, cb); } - Job.find({}, (err, jobs) => { + Job.find(whereFilt, (err, jobs) => { cb(err, jobs); }); } From 2b9e19cff1e19557ad553c2339cd508a51594d79 Mon Sep 17 00:00:00 2001 From: Quincy Larson Date: Fri, 16 Oct 2015 16:12:01 -0700 Subject: [PATCH 13/55] continue improving job board copy --- common/app/routes/Jobs/components/Jobs.jsx | 31 +++++++++++--------- common/app/routes/Jobs/components/List.jsx | 8 +++-- common/app/routes/Jobs/components/NewJob.jsx | 22 +++++++------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index fec91fc342..31327b9ef1 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -66,22 +66,28 @@ export default contain( md={ 10 } mdOffset= { 1 } xs={ 12 }> -

Free Code Camps' Job Board

- +

Job Opportunities

+ -

- Need to find the best junior developers? - Post your job today! + xs={ 12 } + xsOffset={ 0 }> +

+ Talented web developers with strong portfolios are eager + to work for your company.

+ + +
+ Follow @CamperJobs
@@ -89,9 +95,6 @@ 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 368a376294..5bfb32c360 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -21,7 +21,8 @@ export default React.createClass({ company, position, isHighlighted, - postedOn + postedOn, + locale }) => { const className = classnames({ @@ -36,12 +37,15 @@ export default React.createClass({ onClick={ () => handleClick(id) }>

- { company } + { company } {' '} - { position } {' '} + + ({ locale }) +

-

Job Information

+

First, tell us about the position

handleChange('position', e) } - placeholder='Position' + placeholder='e.g. Full Stack Developer, Front End Developer, etc.' required={ true } type='text' value={ position.value } @@ -224,7 +224,7 @@ export default contain({ label='Location' labelClassName={ labelClass } onChange={ (e) => handleChange('locale', e) } - placeholder='Location' + placeholder='e.g. San Francisco, Remote, etc.' required={ true } type='text' value={ locale.value } @@ -234,7 +234,6 @@ export default contain({ label='Description' labelClassName={ labelClass } onChange={ (e) => handleChange('description', e) } - placeholder='Description' required={ true } rows='10' type='textarea' @@ -242,14 +241,13 @@ export default contain({ wrapperClassName={ inputClass } />
-

Company Information

+

Tell us about your organization

handleChange('company', e) } - placeholder='Foo, INC' type='text' value={ company.value } wrapperClassName={ inputClass } /> @@ -258,7 +256,7 @@ export default contain({ label='Email' labelClassName={ labelClass } onChange={ (e) => handleChange('email', e) } - placeholder='Email' + placeholder='you@yourcompany.com' required={ true } type='email' value={ email.value } @@ -268,7 +266,7 @@ export default contain({ label='Phone' labelClassName={ labelClass } onChange={ (e) => handleChange('phone', e) } - placeholder='555-123-1234' + placeholder='555-867-5309' type='tel' value={ phone.value } wrapperClassName={ inputClass } /> @@ -277,7 +275,7 @@ export default contain({ label='URL' labelClassName={ labelClass } onChange={ (e) => handleChange('url', e) } - placeholder='http://freecatphotoapp.com' + placeholder='http://freecodecamp.com' type='url' value={ url.value } wrapperClassName={ inputClass } /> @@ -292,11 +290,11 @@ export default contain({ wrapperClassName={ inputClass } />
-

Make it stand out

+

Highlight your listing to make it stand out

handleForm({ From 2b6a84c1f517ee73e210d32b40e781cebeffcca8 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 16 Oct 2015 20:05:22 -0700 Subject: [PATCH 14/55] Fix lint errors. Add more copy --- client/less/jobs.less | 8 +++++ common/app/routes/Jobs/components/Jobs.jsx | 33 ++++++++++++-------- common/app/routes/Jobs/components/List.jsx | 16 +++++++--- common/app/routes/Jobs/components/NewJob.jsx | 18 ++++++----- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/client/less/jobs.less b/client/less/jobs.less index cb1175f536..7ab32f7a93 100644 --- a/client/less/jobs.less +++ b/client/less/jobs.less @@ -10,3 +10,11 @@ a.jobs-list-highlight:hover { cursor: pointer; cursor: hand; } + +.jobs-checkbox-spacer input[type="checkbox"] { + margin-left: -23px +} + +.jobs-checkbox-spacer label { + padding-left: 130px +} diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 31327b9ef1..d9c7eccdf4 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,36 +1,36 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; -import { History } from 'react-router'; import { Button, Panel, Row, Col } from 'react-bootstrap'; -import CreateJobModal from './CreateJobModal.jsx'; import ListJobs from './List.jsx'; export default contain( { store: 'jobsStore', fetchAction: 'jobActions.getJobs', - actions: 'jobActions' + actions: [ + 'appActions', + 'jobActions' + ] }, React.createClass({ displayName: 'Jobs', - mixins: [History], - propTypes: { children: PropTypes.element, + appActions: PropTypes.object, jobActions: PropTypes.object, jobs: PropTypes.array, showModal: PropTypes.bool }, handleJobClick(id) { - const { jobActions } = this.props; + const { appActions, jobActions } = this.props; if (!id) { return null; } jobActions.findJob(id); - this.history.pushState(null, `/jobs/${id}`); + appActions.goTo(`/jobs/${id}`); }, renderList(handleJobClick, jobs) { @@ -55,8 +55,7 @@ export default contain( const { children, jobs, - showModal, - jobActions + appActions } = this.props; return ( @@ -77,17 +76,25 @@ export default contain(

+ smOffset={ 2 } + xs={ 12 }>
- Follow @CamperJobs + + Follow @CamperJobs +
diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index 5bfb32c360..49d471f097 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -11,6 +11,17 @@ export default React.createClass({ jobs: PropTypes.array }, + addLocation(locale) { + if (!locale) { + return null; + } + return ( + + { locale } - {' '} + + ); + }, + renderJobs(handleClick, jobs = []) { return jobs .filter(({ isPaid, isApproved, isFilled }) => { @@ -42,14 +53,11 @@ export default React.createClass({ - { position } - {' '} - - ({ locale }) -

+ { this.addLocation(locale) } { moment(new Date(postedOn)).format('MMM Do') }

diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 8aa6e05b21..a211c07aed 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -37,6 +37,9 @@ const checkValidity = [ 'company', 'highlight' ]; +const hightlightCopy = ` +Highlight my post to make it stand out. (+$50) +`; function formatValue(value, validator, type = 'string') { const formated = getDefaults(type); @@ -214,7 +217,9 @@ export default contain({ label='Job Title' labelClassName={ labelClass } onChange={ (e) => handleChange('position', e) } - placeholder='e.g. Full Stack Developer, Front End Developer, etc.' + placeholder={ + 'e.g. Full Stack Developer, Front End Developer, etc.' + } required={ true } type='text' value={ position.value } @@ -289,19 +294,18 @@ export default contain({ value={ logo.value } wrapperClassName={ inputClass } /> -
-

Highlight your listing to make it stand out

-
+
handleForm({ highlight: !!checked }) } - type='checkbox' /> + type='checkbox' + wrapperClassName='jobs-checkbox-spacer' />
Date: Mon, 19 Oct 2015 14:19:04 -0700 Subject: [PATCH 15/55] Add twitter follower button to board --- client/less/main.less | 2 +- common/app/routes/Jobs/components/Jobs.jsx | 16 ++-- .../app/routes/Jobs/components/TwitterBtn.jsx | 33 ++++++++ common/app/routes/Jobs/flux/Actions.js | 20 +++++ common/app/routes/Jobs/flux/Store.js | 4 +- common/utils/jsonp$.js | 77 +++++++++++++++++++ 6 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 common/app/routes/Jobs/components/TwitterBtn.jsx create mode 100644 common/utils/jsonp$.js diff --git a/client/less/main.less b/client/less/main.less index b24a4a35b7..35b5491ae2 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -959,7 +959,6 @@ code { z-index: 20000 !important; } -@import "jobs.less"; //uncomment this to see the dimensions of all elements outlined in red //* { @@ -1070,3 +1069,4 @@ code { } @import "chat.less"; +@import "jobs.less"; diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index d9c7eccdf4..bc995ae9c1 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -3,6 +3,7 @@ import { contain } from 'thundercats-react'; import { Button, Panel, Row, Col } from 'react-bootstrap'; import ListJobs from './List.jsx'; +import TwitterBtn from './TwitterBtn.jsx'; export default contain( { @@ -18,12 +19,18 @@ export default contain( propTypes: { children: PropTypes.element, + numOfFollowers: PropTypes.number, appActions: PropTypes.object, jobActions: PropTypes.object, jobs: PropTypes.array, showModal: PropTypes.bool }, + componentDidMount() { + const { jobActions } = this.props; + jobActions.getFollowers(); + }, + handleJobClick(id) { const { appActions, jobActions } = this.props; if (!id) { @@ -54,6 +61,7 @@ export default contain( render() { const { children, + numOfFollowers, jobs, appActions } = this.props; @@ -88,13 +96,7 @@ export default contain( Post a job: $200 for 30 days + weekly tweets
- - Follow @CamperJobs - +
diff --git a/common/app/routes/Jobs/components/TwitterBtn.jsx b/common/app/routes/Jobs/components/TwitterBtn.jsx new file mode 100644 index 0000000000..fb5f8edf12 --- /dev/null +++ b/common/app/routes/Jobs/components/TwitterBtn.jsx @@ -0,0 +1,33 @@ +import React, { createClass, PropTypes } from 'react'; +import { Button } from 'react-bootstrap'; + +const followLink = 'https://twitter.com/intent/follow?' + + 'ref_src=twsrc%5Etfw&region=follow_link&screen_name=CamperJobs&' + + 'amp;tw_p=followbutton'; + +function commify(count) { + return Number(count).toLocaleString('en'); +} + +export default createClass({ + + displayName: 'FollowButton', + + propTypes: { + count: PropTypes.number + }, + + render() { + const { count } = this.props; + return ( + + ); + } +}); diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index d37f16a7f5..d14d83716d 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,6 +1,7 @@ import { Actions } from 'thundercats'; import store from 'store'; import debugFactory from 'debug'; +import { jsonp$ } from '../../../../utils/jsonp$'; const debug = debugFactory('freecc:jobs:actions'); const assign = Object.assign; @@ -59,6 +60,10 @@ export default Actions({ getSavedForm: null, setForm(form) { return { form }; + }, + getFollowers: null, + setFollowersCount(numOfFollowers) { + return { numOfFollowers }; } }) .refs({ displayName: 'JobActions' }) @@ -113,5 +118,20 @@ export default Actions({ appActions.goTo(goTo); }); }); + + jobActions.getFollowers.subscribe(() => { + const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' + + 'info.json?lang=en&screen_names=CamperJobs' + + '&callback=JSONPCallback'; + + jsonp$(url) + .map(({ response }) => { + return response[0]['followers_count']; + }) + .subscribe( + count => jobActions.setFollowersCount(count), + err => jobActions.setError(err) + ); + }); return jobActions; }); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index 8f3acd2949..15aa55b770 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -19,7 +19,8 @@ export default Store({ openModal, closeModal, handleForm, - setForm + setForm, + setFollowersCount } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); @@ -27,6 +28,7 @@ export default Store({ register(setter(openModal)); register(setter(closeModal)); register(setter(setForm)); + register(setter(setFollowersCount)); register(transformer(findJob)); register(handleForm); diff --git a/common/utils/jsonp$.js b/common/utils/jsonp$.js new file mode 100644 index 0000000000..99dbbe2eb4 --- /dev/null +++ b/common/utils/jsonp$.js @@ -0,0 +1,77 @@ +import { AnonymousObservable, Disposable } from 'rx'; + +const root = typeof window !== 'undefined' ? window : {}; +const trash = 'document' in root && root.document.createElement('div'); + +function destroy(element) { + trash.appendChild(element); + trash.innerHTML = ''; +} + +export function jsonp$(options) { + let id = 0; + if (typeof options === 'string') { + options = { url: options }; + } + + return new AnonymousObservable(function(o) { + const settings = Object.assign( + {}, + { + jsonp: 'JSONPCallback', + async: true, + jsonpCallback: 'rxjsjsonpCallbackscallback_' + (id++).toString(36) + }, + options + ); + + let script = root.document.createElement('script'); + script.type = 'text/javascript'; + script.async = settings.async; + script.src = settings.url.replace(settings.jsonp, settings.jsonpCallback); + + root[settings.jsonpCallback] = function(data) { + root[settings.jsonpCallback].called = true; + root[settings.jsonpCallback].data = data; + }; + + const handler = function(e) { + if (e.type === 'load' && !root[settings.jsonpCallback].called) { + e = { type: 'error' }; + } + const status = e.type === 'error' ? 400 : 200; + const data = root[settings.jsonpCallback].data; + + if (status === 200) { + o.onNext({ + status: status, + responseType: 'jsonp', + response: data, + originalEvent: e + }); + + o.onCompleted(); + } else { + o.onError({ + type: 'error', + status: status, + originalEvent: e + }); + } + }; + + script.onload = script.onreadystatechanged = script.onerror = handler; + + const head = root.document.getElementsByTagName('head')[0] || + root.document.documentElement; + + head.insertBefore(script, head.firstChild); + + return Disposable.create(() => { + script.onload = script.onreadystatechanged = script.onerror = null; + + destroy(script); + script = null; + }); + }); +} From 0c6a9bbd71468d685912d461cc37dd770528fb4a Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 19 Oct 2015 14:46:17 -0700 Subject: [PATCH 16/55] Add more copy on show job --- client/less/main.less | 4 ++++ common/app/routes/Jobs/components/List.jsx | 2 +- common/app/routes/Jobs/components/ShowJob.jsx | 22 ++++++++++--------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/client/less/main.less b/client/less/main.less index 35b5491ae2..4a86571c99 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -9,6 +9,10 @@ html,body,div,span,a,li,td,th { font-weight: 300; } +bold { + font-weight: 500; +} + li, .wrappable { white-space: pre; /* CSS 2.0 */ white-space: pre-wrap; /* CSS 2.1 */ diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index 49d471f097..300630684b 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -48,7 +48,7 @@ export default React.createClass({ onClick={ () => handleClick(id) }>

- { company } + { company } {' '} - { position } diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index b1d8faad5a..497b06a5f5 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -1,6 +1,5 @@ import React, { PropTypes } from 'react'; import { Row, Col, Thumbnail, Panel } from 'react-bootstrap'; -import moment from 'moment'; const defaultImage = 'https://pbs.twimg.com/' + @@ -43,7 +42,6 @@ export default React.createClass({ locale, email, phone, - postedOn, description } = job; @@ -63,8 +61,8 @@ export default React.createClass({
+ md={ 2 } + mdOffset={ 3 }> - Position: { position || 'N/A' } + Position: { position || 'N/A' }
- Location: { locale ? locale : `${city}, ${state}` } -
- Contact: { email || phone || 'N/A' } -
- Posted On: { moment(postedOn).format('MMMM Do, YYYY') } + Location: + { locale ? locale : `${city}, ${state}` }
@@ -91,6 +86,13 @@ export default React.createClass({

{ description }

+ + + Contact: { email || phone } + + From 8a02348ddbaf8e1de2fcd1f4a83bc541cdc6a262 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 19 Oct 2015 15:38:48 -0700 Subject: [PATCH 17/55] Clear job from localStorage on submit --- common/app/routes/Jobs/components/Preview.jsx | 1 + common/app/routes/Jobs/flux/Actions.js | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index a6249e4b2a..9c805266ac 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -38,6 +38,7 @@ export default contain( block={ true } className='signup-btn' onClick={ () => { + jobActions.clearSavedForm(); jobActions.saveJobToDb({ goTo: '/jobs/new/check-out', job diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index d14d83716d..9a979671b4 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -58,6 +58,7 @@ export default Actions({ }, saveForm: null, getSavedForm: null, + clearSavedForm: null, setForm(form) { return { form }; }, @@ -107,6 +108,10 @@ export default Actions({ } }); + jobActions.clearSavedForm.subscribe(() => { + store.remove('newJob'); + }); + jobActions.saveJobToDb.subscribe(({ goTo, job }) => { const appActions = cat.getActions('appActions'); services.create('jobs', { job }, null, (err, job) => { @@ -133,5 +138,6 @@ export default Actions({ err => jobActions.setError(err) ); }); + return jobActions; }); From c6c1d7dac4c9667398b7fef9b7283cab155fadaf Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 19 Oct 2015 22:51:30 -0700 Subject: [PATCH 18/55] Add redirects --- .../routes/Jobs/components/JobNotFound.jsx | 31 +++++++++++++++++++ common/app/routes/Jobs/components/Preview.jsx | 15 +++++++++ common/app/routes/Jobs/components/Show.jsx | 28 ++++++++++++++++- common/app/routes/Jobs/utils.js | 7 +++++ server/boot/a-react.js | 3 +- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 common/app/routes/Jobs/components/JobNotFound.jsx diff --git a/common/app/routes/Jobs/components/JobNotFound.jsx b/common/app/routes/Jobs/components/JobNotFound.jsx new file mode 100644 index 0000000000..b921dfd4dc --- /dev/null +++ b/common/app/routes/Jobs/components/JobNotFound.jsx @@ -0,0 +1,31 @@ +import React, { createClass } from 'react'; +import { LinkContainer } from 'react-router-bootstrap'; +import { Button, Row, Col, Panel } from 'react-bootstrap'; + +export default createClass({ + displayName: 'NoJobFound', + + render() { + return ( +
+ + + + No job found... + + + + + + +
+ ); + } +}); diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 9c805266ac..0504794904 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,7 +1,9 @@ import React, { PropTypes } from 'react'; import { Panel, Button, Row, Col } from 'react-bootstrap'; import { contain } from 'thundercats-react'; + import ShowJob from './ShowJob.jsx'; +import JobNotFound from './JobNotFound.jsx'; export default contain( { @@ -23,8 +25,21 @@ export default contain( jobActions: PropTypes.object }, + componentDidMount() { + const { appActions, job } = this.props; + // redirect user in client + if (!job || !job.position || !job.description) { + appActions.goTo('/jobs/new'); + } + }, + render() { const { appActions, job, jobActions } = this.props; + + if (!job || !job.position || job.description) { + return ; + } + return (
diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index ce2512c27d..2c4825bcff 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -1,5 +1,10 @@ +import React, { createClass } from 'react'; +import { History } from 'react-router'; import { contain } from 'thundercats-react'; + import ShowJob from './ShowJob.jsx'; +import JobNotFound from './JobNotFound.jsx'; +import { isJobValid } from '../utils'; export default contain( { @@ -20,5 +25,26 @@ export default contain( return job.id !== id; } }, - ShowJob + createClass({ + displayName: 'Show', + + mixins: [History], + + componentDidMount() { + const { job } = this.props; + // redirect user in client + if (!isJobValid(job)) { + this.history.pushState(null, '/jobs'); + } + }, + + render() { + const { job } = this.props; + + if (!isJobValid(job)) { + return ; + } + return ; + } + }) ); diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js index aeb0396c12..f2fc37d593 100644 --- a/common/app/routes/Jobs/utils.js +++ b/common/app/routes/Jobs/utils.js @@ -20,3 +20,10 @@ export function getDefaults(type, value) { } return Object.assign({}, defaults[type]); } + +export function isJobValid(job) { + return job && + !job.isFilled && + job.isApproved && + job.isPaid; +} diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 9b1f4926c9..31ffc9304c 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -35,7 +35,8 @@ export default function reactSubRouter(app) { // returns a router wrapped app app$({ location }) // if react-router does not find a route send down the chain - .filter(function({ props}) { + .filter(function({ props, nextLocation }) { + console.log('foo', nextLocation); if (!props) { debug('react tried to find %s but got 404', location.pathname); return next(); From 25464c4f9e6e0909d5e54a5d795a500ea0493138 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 19 Oct 2015 23:16:56 -0700 Subject: [PATCH 19/55] Add jobs behind beta flag --- server/boot/a-react.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 31ffc9304c..3171db6553 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -11,17 +11,26 @@ const debug = debugFactory('freecc:react-server'); // add routes here as they slowly get reactified // remove their individual controllers const routes = [ - '/hikes', - '/hikes/*', '/jobs', '/jobs/*' ]; +const devRoutes = [ + '/hikes', + '/hikes/*' +]; + export default function reactSubRouter(app) { var router = app.loopback.Router(); + if (process.env.BETA) { + routes.forEach((route) => { + router.get(route, serveReactApp); + }); + } + if (process.env.NODE_ENV === 'development') { - routes.forEach(function(route) { + devRoutes.forEach(function(route) { router.get(route, serveReactApp); }); } @@ -35,8 +44,7 @@ export default function reactSubRouter(app) { // returns a router wrapped app app$({ location }) // if react-router does not find a route send down the chain - .filter(function({ props, nextLocation }) { - console.log('foo', nextLocation); + .filter(function({ props }) { if (!props) { debug('react tried to find %s but got 404', location.pathname); return next(); From cc736cda4e5af8427041cf0f9a2f3cd70a7d5ea7 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 20 Oct 2015 13:08:14 -0700 Subject: [PATCH 20/55] Add certs/isRemote flags to job form and model --- client/less/jobs.less | 7 +- common/app/routes/Jobs/components/NewJob.jsx | 84 ++++++++++++++++++-- common/models/job.json | 15 ++++ 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/client/less/jobs.less b/client/less/jobs.less index 7ab32f7a93..5fd2bcfc58 100644 --- a/client/less/jobs.less +++ b/client/less/jobs.less @@ -10,11 +10,10 @@ a.jobs-list-highlight:hover { cursor: pointer; cursor: hand; } +.jobs-checkbox { + text-align: left +} .jobs-checkbox-spacer input[type="checkbox"] { margin-left: -23px } - -.jobs-checkbox-spacer label { - padding-left: 130px -} diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index a211c07aed..dbdec6ff1b 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -2,6 +2,8 @@ import React, { PropTypes } from 'react'; import { History } from 'react-router'; import { contain } from 'thundercats-react'; import debugFactory from 'debug'; +import dedent from 'dedent'; + import { getDefaults } from '../utils'; import { @@ -41,6 +43,25 @@ const hightlightCopy = ` Highlight my post to make it stand out. (+$50) `; +const isFullStackCopy = ` +Only allow full stack certified students to apply. +`; + +const isFrontEndCopy = ` +Only allow front end certified students to apply. +`; + +const isRemoteCopy = ` +This job can be done remotely. +`; + +const checkboxClass = dedent` + jobs-checkbox + jobs-checkbox-spacer + col-sm-offset-2 + col-sm-6 col-md-offset-3 +`; + function formatValue(value, validator, type = 'string') { const formated = getDefaults(type); if (validator && type === 'string') { @@ -78,7 +99,10 @@ export default contain({ url, logo, company, - highlight + highlight, + isFullStackCert, + isFrontEndCert, + isRemoteOk } = form; return { position: formatValue(position, makeRequired(isAscii)), @@ -89,7 +113,10 @@ export default contain({ url: formatValue(url, isValidURL), logo: formatValue(logo, isValidURL), company: formatValue(company, makeRequired(isAscii)), - highlight: formatValue(highlight, null, 'bool') + highlight: formatValue(highlight, null, 'bool'), + isFullStackCert: formatValue(isFullStackCert, null, 'bool'), + isFrontEndCert: formatValue(isFrontEndCert, null, 'bool'), + isRemoteOk: formatValue(isRemoteOk, null, 'bool') }; }, subscribeOnWillMount() { @@ -109,7 +136,10 @@ export default contain({ url: PropTypes.object, logo: PropTypes.object, company: PropTypes.object, - highlight: PropTypes.object + highlight: PropTypes.object, + isFullStackCert: PropTypes.object, + isFrontEndCert: PropTypes.object, + isRemoteOk: PropTypes.object }, mixins: [History], @@ -131,6 +161,9 @@ export default contain({ } const { + jobActions, + + // form values position, locale, description, @@ -140,7 +173,9 @@ export default contain({ logo, company, highlight, - jobActions + isFullStackCert, + isFrontEndCert, + isRemoteOk } = this.props; // sanitize user output @@ -153,7 +188,10 @@ export default contain({ url: uriInSingleQuotedAttr(url.value), logo: uriInSingleQuotedAttr(logo.value), company: inHTMLData(company.value), - highlight: !!highlight.value + highlight: !!highlight.value, + isFrontEndCert: !!isFrontEndCert.value, + isFullStackCert: !!isFullStackCert.value, + isRemoteOk: !!isRemoteOk.value }; const job = Object.keys(jobValues).reduce((accu, prop) => { @@ -191,6 +229,9 @@ export default contain({ logo, company, highlight, + isFrontEndCert, + isFullStackCert, + isRemoteOk, jobActions: { handleForm } } = this.props; const { handleChange } = this; @@ -298,14 +339,43 @@ export default contain({ handleForm({ highlight: !!checked }) } type='checkbox' - wrapperClassName='jobs-checkbox-spacer' /> + wrapperClassName={ checkboxClass } /> + handleForm({ + isFrontEndCert: !!checked + }) + } + type='checkbox' + wrapperClassName={ checkboxClass } /> + handleForm({ + isFullStackCert: !!checked + }) + } + type='checkbox' + wrapperClassName={ checkboxClass } /> + handleForm({ + isRemoteOk: !!checked + }) + } + type='checkbox' + wrapperClassName={ checkboxClass } />
Date: Tue, 20 Oct 2015 13:10:25 -0700 Subject: [PATCH 21/55] Fix highlighted flag is under 'isHighlighted' --- common/app/routes/Jobs/components/NewJob.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index dbdec6ff1b..bcf5c17564 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -37,7 +37,7 @@ const checkValidity = [ 'url', 'logo', 'company', - 'highlight' + 'isHighlighted' ]; const hightlightCopy = ` Highlight my post to make it stand out. (+$50) @@ -99,7 +99,7 @@ export default contain({ url, logo, company, - highlight, + isHighlighted, isFullStackCert, isFrontEndCert, isRemoteOk @@ -113,7 +113,7 @@ export default contain({ url: formatValue(url, isValidURL), logo: formatValue(logo, isValidURL), company: formatValue(company, makeRequired(isAscii)), - highlight: formatValue(highlight, null, 'bool'), + isHighlighted: formatValue(isHighlighted, null, 'bool'), isFullStackCert: formatValue(isFullStackCert, null, 'bool'), isFrontEndCert: formatValue(isFrontEndCert, null, 'bool'), isRemoteOk: formatValue(isRemoteOk, null, 'bool') @@ -136,7 +136,7 @@ export default contain({ url: PropTypes.object, logo: PropTypes.object, company: PropTypes.object, - highlight: PropTypes.object, + isHighlighted: PropTypes.object, isFullStackCert: PropTypes.object, isFrontEndCert: PropTypes.object, isRemoteOk: PropTypes.object @@ -172,7 +172,7 @@ export default contain({ url, logo, company, - highlight, + isHighlighted, isFullStackCert, isFrontEndCert, isRemoteOk @@ -188,7 +188,7 @@ export default contain({ url: uriInSingleQuotedAttr(url.value), logo: uriInSingleQuotedAttr(logo.value), company: inHTMLData(company.value), - highlight: !!highlight.value, + isHighlighted: !!isHighlighted.value, isFrontEndCert: !!isFrontEndCert.value, isFullStackCert: !!isFullStackCert.value, isRemoteOk: !!isRemoteOk.value @@ -228,7 +228,7 @@ export default contain({ url, logo, company, - highlight, + isHighlighted, isFrontEndCert, isFullStackCert, isRemoteOk, @@ -337,11 +337,11 @@ export default contain({
handleForm({ - highlight: !!checked + isHighlighted: !!checked }) } type='checkbox' From ac8a1a3f6f4fa35b14cb7126017a26c70ae017ab Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 20 Oct 2015 15:01:05 -0700 Subject: [PATCH 22/55] Finish copy, add flags --- .../app/routes/Jobs/components/GoToPayPal.jsx | 7 +- common/app/routes/Jobs/components/NewJob.jsx | 139 ++++++++---------- .../Jobs/components/NewJobCompleted.jsx | 15 +- common/app/routes/Jobs/components/Preview.jsx | 2 +- 4 files changed, 75 insertions(+), 88 deletions(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index a29ecbe976..229c86ddb4 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -19,7 +19,7 @@ export default contain( }, render() { - const { id } = this.props; + const { id, isHighlighted } = this.props; return (
@@ -58,7 +58,10 @@ export default contain( + value={ isHighlighted ? + '' : + 'ZVU498PLMPHKU' + } /> !!val && validator(val); } @@ -95,7 +93,6 @@ export default contain({ locale, description, email, - phone, url, logo, company, @@ -109,7 +106,6 @@ export default contain({ locale: formatValue(locale, makeRequired(isAscii)), description: formatValue(description, makeRequired(isAscii)), email: formatValue(email, makeRequired(isEmail)), - phone: formatValue(phone, isValidPhone), url: formatValue(url, isValidURL), logo: formatValue(logo, isValidURL), company: formatValue(company, makeRequired(isAscii)), @@ -132,7 +128,6 @@ export default contain({ locale: PropTypes.object, description: PropTypes.object, email: PropTypes.object, - phone: PropTypes.object, url: PropTypes.object, logo: PropTypes.object, company: PropTypes.object, @@ -168,7 +163,6 @@ export default contain({ locale, description, email, - phone, url, logo, company, @@ -184,7 +178,6 @@ export default contain({ locale: inHTMLData(locale.value), description: inHTMLData(description.value), email: inHTMLData(email.value), - phone: inHTMLData(phone.value), url: uriInSingleQuotedAttr(url.value), logo: uriInSingleQuotedAttr(logo.value), company: inHTMLData(company.value), @@ -224,7 +217,6 @@ export default contain({ locale, description, email, - phone, url, logo, company, @@ -285,67 +277,6 @@ export default contain({ type='textarea' value={ description.value } wrapperClassName={ inputClass } /> - -
-

Tell us about your organization

-
- handleChange('company', e) } - type='text' - value={ company.value } - wrapperClassName={ inputClass } /> - handleChange('email', e) } - placeholder='you@yourcompany.com' - required={ true } - type='email' - value={ email.value } - wrapperClassName={ inputClass } /> - handleChange('phone', e) } - placeholder='555-867-5309' - type='tel' - value={ phone.value } - wrapperClassName={ inputClass } /> - handleChange('url', e) } - placeholder='http://freecodecamp.com' - type='url' - value={ url.value } - wrapperClassName={ inputClass } /> - handleChange('logo', e) } - placeholder='http://freecatphotoapp.com/logo.png' - type='url' - value={ logo.value } - wrapperClassName={ inputClass } /> - -
- handleForm({ - isHighlighted: !!checked - }) - } - type='checkbox' - wrapperClassName={ checkboxClass } /> + + * { foo } + +
+ +
+

Tell us about your organization

+
+ handleChange('company', e) } + type='text' + value={ company.value } + wrapperClassName={ inputClass } /> + handleChange('email', e) } + placeholder='This is how we will contact you' + required={ true } + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + handleChange('url', e) } + placeholder='http://yourcompany.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> + handleChange('logo', e) } + placeholder='http://yourcompany.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+ handleForm({ + isHighlighted: !!checked + }) + } + type='checkbox' + wrapperClassName={ checkboxClass } />
+
-

- Job under review +

+ Your Position has Been Submitted

- Congrats! Your job has been posted and is under review. - Once we review you job post we will publish it and you will receive - an email from us with a link to the listing. + We’ll review your listing and email you when it’s live. +
+ Thank you for listing this job with Free Code Camp.
diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 0504794904..8f64906d6a 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -36,7 +36,7 @@ export default contain( render() { const { appActions, job, jobActions } = this.props; - if (!job || !job.position || job.description) { + if (!job || !job.position || !job.description) { return ; } From 24927748e0fcfa0e5a321348f8439c50f57bed1c Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 20 Oct 2015 15:30:25 -0700 Subject: [PATCH 23/55] Move highlighting around --- client/less/jobs.less | 3 -- common/app/routes/Jobs/components/NewJob.jsx | 48 +++++++++++++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/client/less/jobs.less b/client/less/jobs.less index 5fd2bcfc58..e1d20aa500 100644 --- a/client/less/jobs.less +++ b/client/less/jobs.less @@ -10,9 +10,6 @@ a.jobs-list-highlight:hover { cursor: pointer; cursor: hand; } -.jobs-checkbox { - text-align: left -} .jobs-checkbox-spacer input[type="checkbox"] { margin-left: -23px diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index d8301e894e..8399637482 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -16,7 +16,8 @@ import { Col, Input, Row, - Panel + Panel, + Well } from 'react-bootstrap'; import { @@ -58,7 +59,7 @@ This job can be performed remotely. `; const checkboxClass = dedent` - jobs-checkbox + text-left jobs-checkbox-spacer col-sm-offset-2 col-sm-6 col-md-offset-3 @@ -353,19 +354,42 @@ export default contain({ wrapperClassName={ inputClass } />
- handleForm({ - isHighlighted: !!checked - }) - } - type='checkbox' - wrapperClassName={ checkboxClass } /> + +
+

Make it stand out

+
+
+ + + Highlight this ad to give it extra attention. +
+ Featured listings receive more clicks and more applications. + +
+
+ + handleForm({ + isHighlighted: !!checked + }) + } + type='checkbox' + wrapperClassName={ + checkboxClass.replace('text-left', '') + } /> + +
+ + +
+ ); + }, + render() { - const { id, isHighlighted, buttonId, price, discountAmount } = this.props; + const { + id, + isHighlighted, + buttonId, + price, + discountAmount + } = this.props; + return (
@@ -124,6 +207,7 @@ export default contain( + { this.renderPromo() }
{ + const body = { code: code.replace(/[^\d\w\s]/, '') }; + if (type) { + body.type = type; + } + postJSON$('/api/promos/getButton', body) + .pluck('response') + .subscribe( + ({ promo }) => { + if (promo && promo.buttonId) { + jobActions.applyPromo(promo); + } + jobActions.setError(new Error('no promo found')); + }, + jobActions.setError + ); + }); + return jobActions; - }); + }); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index 15aa55b770..90e7e26598 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -20,7 +20,9 @@ export default Store({ closeModal, handleForm, setForm, - setFollowersCount + setFollowersCount, + setPromoCode, + applyPromo } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); @@ -28,6 +30,8 @@ export default Store({ register(setter(openModal)); register(setter(closeModal)); register(setter(setForm)); + register(setter(setPromoCode)); + register(setter(applyPromo)); register(setter(setFollowersCount)); register(transformer(findJob)); diff --git a/common/models/promo.js b/common/models/promo.js index 617635f0c6..1901e47d2a 100644 --- a/common/models/promo.js +++ b/common/models/promo.js @@ -1,8 +1,7 @@ import { isAlphanumeric } from 'validator'; export default function promo(Promo) { - Promo.getButton = function getButton(code, type = null) { - + Promo.getButton = function getButton(code, type = 'isNot') { if ( !isAlphanumeric(code) && type && @@ -15,8 +14,7 @@ export default function promo(Promo) { const query = { where: { - code, - type + and: [{ code }, { type }] } }; diff --git a/common/utils/ajax-stream.js b/common/utils/ajax-stream.js index 63b55e76ce..3bdcc2abdc 100644 --- a/common/utils/ajax-stream.js +++ b/common/utils/ajax-stream.js @@ -255,6 +255,7 @@ export function postJSON$(url, body) { url, body: JSON.stringify(body), method: 'POST', + responseType: 'json', headers: { 'Content-Type': 'application/json' } }); } @@ -277,10 +278,7 @@ export function get$(url) { * @returns {Observable} The observable sequence which contains the parsed JSON */ export function getJSON$(url) { - if (!root.JSON && typeof root.JSON.parse !== 'function') { - throw new TypeError('JSON is not supported in your runtime.'); - } - return ajax$({url: url, responseType: 'json'}).map(function(x) { + return ajax$({ url: url, responseType: 'json' }).map(function(x) { return x.response; }); } From 8024a5bd71000dba99bccf7d45cec08134914fc6 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 26 Oct 2015 18:00:33 -0700 Subject: [PATCH 32/55] Clear discount on transition --- common/app/routes/Jobs/components/GoToPayPal.jsx | 4 ++-- common/app/routes/Jobs/components/Preview.jsx | 8 ++++++++ common/app/routes/Jobs/flux/Actions.js | 10 ++++++++++ common/app/routes/Jobs/flux/Store.js | 4 +++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 5f0453cb92..c50b52df92 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -188,11 +188,11 @@ export default contain( -

+{ price }

+

+ { price }

- { this.renderDiscount(discountAmount) } { this.renderHighlightPrice(isHighlighted) } + { this.renderDiscount(discountAmount) } Date: Mon, 26 Oct 2015 18:02:55 -0700 Subject: [PATCH 33/55] Remove debugging comment --- common/app/routes/Jobs/components/Preview.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index b0c75a5b7c..34a4beec14 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -61,7 +61,7 @@ export default contain( block={ true } className='signup-btn' onClick={ () => { - // jobActions.clearSavedForm(); + jobActions.clearSavedForm(); jobActions.saveJobToDb({ goTo: '/jobs/new/check-out', job From 8bce2c9f8b310feaabc20f5d6a38fd7f54a888f4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 26 Oct 2015 18:40:48 -0700 Subject: [PATCH 34/55] Fix styling on PayPal button --- common/app/routes/Jobs/components/GoToPayPal.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index c50b52df92..f6d82b6331 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -132,14 +132,13 @@ export default contain( md={ 3 }> From 3ee4a3fc48f01416fa10c0f92e503c17827d7d3b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 27 Oct 2015 10:21:51 -0700 Subject: [PATCH 35/55] Switch between highlighted buttons --- common/app/routes/Jobs/components/GoToPayPal.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index f6d82b6331..3345e16d08 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -15,7 +15,9 @@ export default contain( actions: 'jobActions', map({ job: { id, isHighlighted } = {}, - buttonId = paypalIds.regular, + buttonId = isHighlighted ? + paypalIds.highlighted : + paypalIds.regular, price = 200, discountAmount = 0, promoCode = '', From 7ac7a4ccfcc8c35d91f7e8b441c6b93fc0112b40 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 27 Oct 2015 15:40:04 -0700 Subject: [PATCH 36/55] Add react links to nav bar --- common/app/components/Flash/Queue.jsx | 27 +++++++++++++ common/app/components/Flash/index.jsx | 0 common/app/components/Nav/Nav.jsx | 57 ++++++++++++++++----------- common/app/components/Nav/NavItem.jsx | 22 +++++++---- common/app/components/Nav/links.json | 3 +- 5 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 common/app/components/Flash/Queue.jsx create mode 100644 common/app/components/Flash/index.jsx diff --git a/common/app/components/Flash/Queue.jsx b/common/app/components/Flash/Queue.jsx new file mode 100644 index 0000000000..718733a923 --- /dev/null +++ b/common/app/components/Flash/Queue.jsx @@ -0,0 +1,27 @@ +import React, { createClass, PropTypes } from 'react'; +import { Alert } from 'react-bootstrap'; + +export default createClass({ + displayName: 'FlashQueue', + + propTypes: { + messages: PropTypes.array + }, + + renderMessages(messages) { + return messages.map(message => { + return ( + + ); + }); + }, + + render() { + const { messages = [] } = this.props; + return ( +
+ { this.renderMessages(messages) } +
+ ); + } +}); diff --git a/common/app/components/Flash/index.jsx b/common/app/components/Flash/index.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/app/components/Nav/Nav.jsx b/common/app/components/Nav/Nav.jsx index ad7f81f7b8..a199d0a539 100644 --- a/common/app/components/Nav/Nav.jsx +++ b/common/app/components/Nav/Nav.jsx @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import { LinkContainer } from 'react-router-bootstrap'; import { Col, CollapsibleNav, @@ -11,16 +12,6 @@ import navLinks from './links.json'; import FCCNavItem from './NavItem.jsx'; const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; -const navElements = navLinks.map((navItem, index) => { - return ( - - { navItem.content } - - ); -}); const logoElement = ( @@ -39,18 +30,40 @@ const toggleButton = ( ); -export default class extends React.Component { - constructor(props) { - super(props); - } +export default React.createClass({ + displayName: 'Nav', - static displayName = 'Nav' - static propTypes = { + propTypes: { points: PropTypes.number, picture: PropTypes.string, signedIn: PropTypes.bool, username: PropTypes.string - } + }, + + renderLinks() { + return navLinks.map(({ content, link, react }, index) => { + if (react) { + return ( + + + { content } + + + ); + } + return ( + + { content } + + ); + }); + }, renderPoints(username, points) { if (!username) { @@ -62,7 +75,7 @@ export default class extends React.Component { [ { points } ] ); - } + }, renderSignin(username, picture) { if (username) { @@ -87,7 +100,7 @@ export default class extends React.Component { ); } - } + }, render() { const { username, points, picture } = this.props; @@ -103,12 +116,12 @@ export default class extends React.Component { className='hamburger-dropdown' navbar={ true } right={ true }> - { navElements } - { this.renderPoints(username, points)} + { this.renderLinks() } + { this.renderPoints(username, points) } { this.renderSignin(username, picture) } ); } -} +}); diff --git a/common/app/components/Nav/NavItem.jsx b/common/app/components/Nav/NavItem.jsx index eec245e7d8..383a7a646e 100644 --- a/common/app/components/Nav/NavItem.jsx +++ b/common/app/components/Nav/NavItem.jsx @@ -4,11 +4,14 @@ import BootstrapMixin from 'react-bootstrap/lib/BootstrapMixin'; export default React.createClass({ displayName: 'FCCNavItem', + mixins: [BootstrapMixin], propTypes: { active: React.PropTypes.bool, 'aria-controls': React.PropTypes.string, + children: React.PropTypes.node, + className: React.PropTypes.string, disabled: React.PropTypes.bool, eventKey: React.PropTypes.any, href: React.PropTypes.string, @@ -30,7 +33,11 @@ export default React.createClass({ e.preventDefault(); if (!this.props.disabled) { - this.props.onSelect(this.props.eventKey, this.props.href, this.props.target); + this.props.onSelect( + this.props.eventKey, + this.props.href, + this.props.target + ); } } }, @@ -50,10 +57,11 @@ export default React.createClass({ ...props } = this.props; - let classes = { - active, - disabled - }; + const linkClassName = classNames(className, { + // 'active': active, we don't actually use the active class + // but it is used for a11y below + 'disabled': disabled + }); let linkProps = { role, @@ -75,9 +83,9 @@ export default React.createClass({ role='presentation'> + aria-selected={ active } + className={ linkClassName }> { children } diff --git a/common/app/components/Nav/links.json b/common/app/components/Nav/links.json index 4baea28e78..175e5170cf 100644 --- a/common/app/components/Nav/links.json +++ b/common/app/components/Nav/links.json @@ -9,5 +9,6 @@ "link": "/news" },{ "content": "Jobs", - "link": "/jobs" + "link": "/jobs", + "react": true }] From d1242d8d43abd6d0fc598ac169b4aa09893daf11 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 27 Oct 2015 17:15:48 -0700 Subject: [PATCH 37/55] Make url protocol-less --- common/app/routes/Jobs/components/NewJob.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index ecb66f2f71..4f19e170fc 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -84,7 +84,7 @@ function formatValue(value, validator, type = 'string') { } function isValidURL(data) { - return isURL(data, { 'require_protocol': true }); + return isURL(data, { 'require_protocol': false }); } function makeRequired(validator) { From 2dd16796d7a0f8a126e4725897dda3226e9f7cb1 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 27 Oct 2015 17:28:11 -0700 Subject: [PATCH 38/55] Change default image Make url link start with http:// --- common/app/routes/Jobs/components/NewJob.jsx | 4 ++-- common/app/routes/Jobs/components/ShowJob.jsx | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 4f19e170fc..3da9ca8f3b 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -84,7 +84,7 @@ function formatValue(value, validator, type = 'string') { } function isValidURL(data) { - return isURL(data, { 'require_protocol': false }); + return isURL(data, { 'require_protocol': true }); } function makeRequired(validator) { @@ -100,7 +100,7 @@ export default contain({ locale, description, email, - url, + url = 'http://', logo, company, isHighlighted, diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index 497b06a5f5..0f2b3a0a0e 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -2,8 +2,7 @@ import React, { PropTypes } from 'react'; import { Row, Col, Thumbnail, Panel } from 'react-bootstrap'; const defaultImage = - 'https://pbs.twimg.com/' + - 'profile_images/562385977390272512/AK29YaTf_400x400.png'; + 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png'; const thumbnailStyle = { backgroundColor: 'white', From d633f74ff9183fcc275d2a35f4a2fa588f1c5b2b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 27 Oct 2015 20:07:28 -0700 Subject: [PATCH 39/55] Change goToPayPal flow --- client/index.js | 16 +++++++++++++++- common/app/Cat.js | 4 ++-- common/app/flux/Actions.js | 14 ++++---------- common/app/flux/Store.js | 4 ++-- common/app/routes/Jobs/components/GoToPayPal.jsx | 14 ++++++++++++-- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/client/index.js b/client/index.js index 73b241041f..a438cd3167 100644 --- a/client/index.js +++ b/client/index.js @@ -27,7 +27,7 @@ app$({ history, location: appLocation }) .flatMap( ({ AppCat }) => { // instantiate the cat with service - const appCat = AppCat(null, services, history); + const appCat = AppCat(null, services); // hydrate the stores return hydrate(appCat, catState) .map(() => appCat); @@ -36,6 +36,20 @@ app$({ history, location: appLocation }) // redirects in the future ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) + .doOnNext(({ appCat }) => { + const appStore = appCat.getStore('appStore'); + const appActions = appCat.getActions('appActions'); + + appStore + .distinctUntilChanged() + .subscribe(function({ route = appLocation.pathname }) { + history.pushState(null, route); + }); + + appActions.goBack.subscribe(function() { + history.goBack(); + }); + }) .flatMap(({ props, appCat }) => { props.history = history; return Render( diff --git a/common/app/Cat.js b/common/app/Cat.js index 6027fa9f2d..31dae9d294 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -5,8 +5,8 @@ import { HikesActions, HikesStore } from './routes/Hikes/flux'; import { JobActions, JobsStore} from './routes/Jobs/flux'; export default Cat() - .init(({ instance: cat, args: [services, history] }) => { - cat.register(AppActions, null, services, history); + .init(({ instance: cat, args: [services] }) => { + cat.register(AppActions, null, services); cat.register(AppStore, null, cat); cat.register(HikesActions, null, services); diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 2c4545afaf..1de396d835 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -17,19 +17,13 @@ export default Actions({ }, getUser: null, - goTo: null, + goTo(route) { + return { route }; + }, goBack: null }) .refs({ displayName: 'AppActions' }) - .init(({ instance: appActions, args: [services, history] }) => { - appActions.goTo.subscribe((url) => { - history.pushState(null, url); - }); - - appActions.goBack.subscribe(() => { - history.goBack(); - }); - + .init(({ instance: appActions, args: [services] }) => { appActions.getUser.subscribe(({ isPrimed }) => { if (isPrimed) { debug('isPrimed'); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index 30ee4e7ff3..5891c17b8f 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -14,10 +14,10 @@ export default Store({ value: initValue }, init({ instance: appStore, args: [cat] }) { - const { setUser, setTitle } = cat.getActions('appActions'); + const { goTo, setUser, setTitle } = cat.getActions('appActions'); const register = createRegistrar(appStore); - register(setter(fromMany(setUser, setTitle))); + register(setter(fromMany(setUser, setTitle, goTo))); return appStore; } diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 3345e16d08..59b33cc32b 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -12,7 +12,10 @@ const paypalIds = { export default contain( { store: 'JobsStore', - actions: 'jobActions', + actions: [ + 'jobActions', + 'appActions' + ], map({ job: { id, isHighlighted } = {}, buttonId = isHighlighted ? @@ -40,6 +43,7 @@ export default contain( displayName: 'GoToPayPal', propTypes: { + appActions: PropTypes.object, id: PropTypes.string, isHighlighted: PropTypes.bool, buttonId: PropTypes.string, @@ -51,6 +55,11 @@ export default contain( jobActions: PropTypes.object }, + goToJobBoard() { + const { appActions } = this.props; + appActions.goTo('/jobs'); + }, + renderDiscount(discountAmount) { if (!discountAmount) { return null; @@ -217,7 +226,8 @@ export default contain( + onClick={ this.goToJobBoard } + target='_blank'> Date: Tue, 27 Oct 2015 23:46:42 -0700 Subject: [PATCH 40/55] Fix issue with transitioning between routes --- client/index.js | 32 +++++++++++++++---- common/app/flux/Actions.js | 2 +- common/app/flux/Store.js | 4 +-- .../app/routes/Jobs/components/GoToPayPal.jsx | 2 +- common/app/routes/Jobs/components/Jobs.jsx | 4 +-- common/app/routes/Jobs/components/Preview.jsx | 2 +- common/app/routes/Jobs/flux/Actions.js | 2 +- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/client/index.js b/client/index.js index a438cd3167..d76e2a78ef 100644 --- a/client/index.js +++ b/client/index.js @@ -22,6 +22,19 @@ const history = createHistory(); const appLocation = createLocation( location.pathname + location.search ); + +function location$(history) { + return Rx.Observable.create(function(observer) { + const dispose = history.listen(function(location) { + observer.onNext(location.pathname); + }); + + return Rx.Disposable.create(() => { + dispose(); + }); + }); +} + // returns an observable app$({ history, location: appLocation }) .flatMap( @@ -37,18 +50,25 @@ app$({ history, location: appLocation }) ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) .doOnNext(({ appCat }) => { - const appStore = appCat.getStore('appStore'); const appActions = appCat.getActions('appActions'); - appStore + location$(history) + .pluck('pathname') .distinctUntilChanged() - .subscribe(function({ route = appLocation.pathname }) { - history.pushState(null, route); - }); + .doOnNext(route => debug('route change', route)) + .subscribe(route => appActions.updateRoute(route)); appActions.goBack.subscribe(function() { history.goBack(); }); + + appActions + .updateRoute + .pluck('route') + .doOnNext(route => debug('update route', route)) + .subscribe(function(route) { + history.pushState(null, route); + }); }) .flatMap(({ props, appCat }) => { props.history = history; @@ -63,7 +83,7 @@ app$({ history, location: appLocation }) debug('react rendered'); }, err => { - debug('an error has occured', err.stack); + throw err; }, () => { debug('react closed subscription'); diff --git a/common/app/flux/Actions.js b/common/app/flux/Actions.js index 1de396d835..6f4754224e 100644 --- a/common/app/flux/Actions.js +++ b/common/app/flux/Actions.js @@ -17,7 +17,7 @@ export default Actions({ }, getUser: null, - goTo(route) { + updateRoute(route) { return { route }; }, goBack: null diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index 5891c17b8f..33741a1165 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -14,10 +14,10 @@ export default Store({ value: initValue }, init({ instance: appStore, args: [cat] }) { - const { goTo, setUser, setTitle } = cat.getActions('appActions'); + const { updateRoute, setUser, setTitle } = cat.getActions('appActions'); const register = createRegistrar(appStore); - register(setter(fromMany(setUser, setTitle, goTo))); + register(setter(fromMany(setUser, setTitle, updateRoute))); return appStore; } diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 59b33cc32b..3e7d906209 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -57,7 +57,7 @@ export default contain( goToJobBoard() { const { appActions } = this.props; - appActions.goTo('/jobs'); + appActions.updateRoute('/jobs'); }, renderDiscount(discountAmount) { diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index bc995ae9c1..8810bfa941 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -37,7 +37,7 @@ export default contain( return null; } jobActions.findJob(id); - appActions.goTo(`/jobs/${id}`); + appActions.updateRoute(`/jobs/${id}`); }, renderList(handleJobClick, jobs) { @@ -91,7 +91,7 @@ export default contain( bsSize='large' className='signup-btn btn-block' onClick={ ()=> { - appActions.goTo('/jobs/new'); + appActions.updateRoute('/jobs/new'); }}> Post a job: $200 for 30 days + weekly tweets diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 34a4beec14..c3cb67c170 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -32,7 +32,7 @@ export default contain( const { appActions, job } = this.props; // redirect user in client if (!job || !job.position || !job.description) { - appActions.goTo('/jobs/new'); + appActions.updateRoute('/jobs/new'); } }, diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 72bad3ed2a..ced8f5d723 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -151,7 +151,7 @@ export default Actions({ return jobActions.setError(err); } jobActions.setJobs({ job }); - appActions.goTo(goTo); + appActions.updateRoute(goTo); }); }); From 2cb305cc2c5b7d6144e99ca982a390c5970e80df Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 29 Oct 2015 16:43:35 -0700 Subject: [PATCH 41/55] Add real paypal buttons --- common/app/routes/Jobs/components/GoToPayPal.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 3e7d906209..f372a05aa5 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -2,11 +2,11 @@ import React, { PropTypes } from 'react'; import { Button, Input, Col, Panel, Row, Well } from 'react-bootstrap'; import { contain } from 'thundercats-react'; +// real paypal buttons +// will take your money const paypalIds = { - regular: 'ZVU498PLMPHKU', - regularDiscount: '58U7P36W3L2GQ', - highlighted: '3YYSTBAMJYTUW', - highlightedDiscount: 'QGWTUZ9XEE6EL' + regular: 'Q8Z82ZLAX3Q8N', + highlighted: 'VC8QPSKCYMZLN' }; export default contain( From e1a398597ddf6910e6a6f01fe90ba688d941cd61 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 29 Oct 2015 16:46:26 -0700 Subject: [PATCH 42/55] Prevent binding attacks --- server/services/job.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/services/job.js b/server/services/job.js index a3807a6f28..0e81c08788 100644 --- a/server/services/job.js +++ b/server/services/job.js @@ -15,6 +15,12 @@ export default function getJobServices(app) { if (!job) { return cb(new Error('job creation should get a job object')); } + + Object.assign(job, { + isPaid: false, + isApproved: false + }); + Job.create(job, (err, savedJob) => { cb(err, savedJob); }); From 134a3a52e8357884d2ad14ba10db145832b94093 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 29 Oct 2015 17:09:26 -0700 Subject: [PATCH 43/55] Fix lint issue --- server/services/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/user.js b/server/services/user.js index 5f293bfee0..3fa4dc40c1 100644 --- a/server/services/user.js +++ b/server/services/user.js @@ -9,7 +9,7 @@ const protectedUserFields = { profiles: censor }; -export default function userServices(/* app */) { +export default function userServices() { return { name: 'user', read: (req, resource, params, config, cb) => { From c354ddf1ac340be0a5651f5a1917f2323238a60f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 29 Oct 2015 17:55:42 -0700 Subject: [PATCH 44/55] Expose jobs board to production. --- server/boot/a-react.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 3171db6553..d4a000067f 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -23,11 +23,10 @@ const devRoutes = [ export default function reactSubRouter(app) { var router = app.loopback.Router(); - if (process.env.BETA) { - routes.forEach((route) => { - router.get(route, serveReactApp); - }); - } + // These routes are in production + routes.forEach((route) => { + router.get(route, serveReactApp); + }); if (process.env.NODE_ENV === 'development') { devRoutes.forEach(function(route) { From 809b74307c4ac9c55ac8be3c946a55b6cdd53614 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 12:41:00 -0700 Subject: [PATCH 45/55] Remove navbar active color --- client/less/lib/bootstrap/navbar.less | 1 - 1 file changed, 1 deletion(-) diff --git a/client/less/lib/bootstrap/navbar.less b/client/less/lib/bootstrap/navbar.less index 7a0753c175..c0ffe7f998 100755 --- a/client/less/lib/bootstrap/navbar.less +++ b/client/less/lib/bootstrap/navbar.less @@ -424,7 +424,6 @@ &:hover, &:focus { color: @navbar-default-link-active-color; - background-color: @navbar-default-link-active-bg; } } > .disabled > a { From a8f2bb1b5032efae21c29cdf4924275826559425 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 12:55:13 -0700 Subject: [PATCH 46/55] Fix font --- common/app/routes/Jobs/components/Jobs.jsx | 13 ++---- server/views/layout-react.jade | 7 ++-- ...small-head.jade => react-stylesheets.jade} | 6 +-- server/views/redirect-https.jade | 42 +++++++++++++------ 4 files changed, 38 insertions(+), 30 deletions(-) rename server/views/partials/{small-head.jade => react-stylesheets.jade} (76%) diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 8810bfa941..09c03030c7 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -73,16 +73,11 @@ export default contain( md={ 10 } mdOffset= { 1 } xs={ 12 }> -

Job Opportunities

+

+ Talented web developers with strong portfolios are eager + to work for your company +

- -

- Talented web developers with strong portfolios are eager - to work for your company. -

- Date: Fri, 30 Oct 2015 13:06:36 -0700 Subject: [PATCH 47/55] Add How Do I apply to job and remove contact --- common/app/routes/Jobs/components/ShowJob.jsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index 0f2b3a0a0e..42a0d29286 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -39,9 +39,8 @@ export default React.createClass({ company, state, locale, - email, - phone, - description + description, + howToApply } = job; return ( @@ -89,7 +88,7 @@ export default React.createClass({ - Contact: { email || phone } + How do I apply? { howToApply }
From 2a04b02d037ed0a59faee6443bc2355eb146aeb3 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 14:00:07 -0700 Subject: [PATCH 48/55] Add http to url or logo when not present --- common/app/routes/Jobs/components/NewJob.jsx | 22 +++++++++++++++++--- package.json | 5 +++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 3da9ca8f3b..7a2c61f7a5 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -3,6 +3,7 @@ import { History } from 'react-router'; import { contain } from 'thundercats-react'; import debugFactory from 'debug'; import dedent from 'dedent'; +import normalizeUrl from 'normalize-url'; import { getDefaults } from '../utils'; @@ -83,6 +84,21 @@ function formatValue(value, validator, type = 'string') { return formated; } +const normalizeOptions = { + stripWWW: false +}; + +function formatUrl(url) { + if ( + typeof url === 'string' && + url.length > 4 && + url.indexOf('.') !== -1 + ) { + return normalizeUrl(url, normalizeOptions); + } + return url; +} + function isValidURL(data) { return isURL(data, { 'require_protocol': true }); } @@ -100,7 +116,7 @@ export default contain({ locale, description, email, - url = 'http://', + url, logo, company, isHighlighted, @@ -114,8 +130,8 @@ export default contain({ locale: formatValue(locale, makeRequired(isAscii)), description: formatValue(description, makeRequired(isAscii)), email: formatValue(email, makeRequired(isEmail)), - url: formatValue(url, isValidURL), - logo: formatValue(logo, isValidURL), + url: formatValue(formatUrl(url), isValidURL), + logo: formatValue(formatUrl(logo), isValidURL), company: formatValue(company, makeRequired(isAscii)), isHighlighted: formatValue(isHighlighted, null, 'bool'), isFullStackCert: formatValue(isFullStackCert, null, 'bool'), diff --git a/package.json b/package.json index c4777546c9..10697a1778 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,11 @@ "gulp-eslint": "~0.9.0", "gulp-inject": "~1.0.2", "gulp-jsonlint": "^1.1.0", + "gulp-less": "^3.0.3", + "gulp-minify-css": "~0.5.1", "gulp-nodemon": "^2.0.3", "gulp-notify": "^2.2.0", "gulp-plumber": "^1.0.1", - "gulp-less": "^3.0.3", - "gulp-minify-css": "~0.5.1", "gulp-reduce-file": "0.0.1", "gulp-rev": "^6.0.1", "gulp-rev-replace": "^0.4.2", @@ -85,6 +85,7 @@ "node-slack": "0.0.7", "node-uuid": "^1.4.3", "nodemailer": "~1.3.0", + "normalize-url": "^1.3.1", "object.assign": "^3.0.0", "passport-facebook": "^2.0.0", "passport-github": "^0.1.5", From fedbd3c72572bd0598f652df19cc200aa95780ee Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 14:03:54 -0700 Subject: [PATCH 49/55] Fix paypal button url --- common/app/routes/Jobs/components/GoToPayPal.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index f372a05aa5..c5bc9ce451 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -224,7 +224,7 @@ export default contain( md={ 6 } mdOffset={ 3 }> From 33a13982446cf2f0dba8f3132f31e1b448417d00 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 14:20:20 -0700 Subject: [PATCH 50/55] Fix leave trailing slash when entering urls --- common/app/routes/Jobs/components/NewJob.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 7a2c61f7a5..cd5cbb237a 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -94,7 +94,12 @@ function formatUrl(url) { url.length > 4 && url.indexOf('.') !== -1 ) { - return normalizeUrl(url, normalizeOptions); + // prevent trailing / from being stripped during typing + let lastChar = ''; + if (url.substring(url.length - 1) === '/') { + lastChar = '/'; + } + return normalizeUrl(url, normalizeOptions) + lastChar; } return url; } From 50701b3dde69c4088bd4c12e33a2f67cdb2dbe8d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 14:43:38 -0700 Subject: [PATCH 51/55] Remove trailing slash on submit --- common/app/routes/Jobs/components/NewJob.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index cd5cbb237a..fea39cf6d6 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -88,7 +88,7 @@ const normalizeOptions = { stripWWW: false }; -function formatUrl(url) { +function formatUrl(url, shouldKeepTrailingSlash = true) { if ( typeof url === 'string' && url.length > 4 && @@ -96,7 +96,7 @@ function formatUrl(url) { ) { // prevent trailing / from being stripped during typing let lastChar = ''; - if (url.substring(url.length - 1) === '/') { + if (shouldKeepTrailingSlash && url.substring(url.length - 1) === '/') { lastChar = '/'; } return normalizeUrl(url, normalizeOptions) + lastChar; @@ -210,8 +210,8 @@ export default contain({ locale: inHTMLData(locale.value), description: inHTMLData(description.value), email: inHTMLData(email.value), - url: uriInSingleQuotedAttr(url.value), - logo: uriInSingleQuotedAttr(logo.value), + url: formatUrl(uriInSingleQuotedAttr(url.value), false), + logo: formatUrl(uriInSingleQuotedAttr(logo.value), false), company: inHTMLData(company.value), isHighlighted: !!isHighlighted.value, isFrontEndCert: !!isFrontEndCert.value, From 022dc4dd7bc5f862a59e10d5ccabe681e764946a Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 15:01:01 -0700 Subject: [PATCH 52/55] Remove ascii filtering from description --- common/app/routes/Jobs/components/NewJob.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index fea39cf6d6..e4f01d73c4 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,3 +1,4 @@ +import { helpers } from 'rx'; import React, { PropTypes } from 'react'; import { History } from 'react-router'; import { contain } from 'thundercats-react'; @@ -133,7 +134,7 @@ export default contain({ return { position: formatValue(position, makeRequired(isAscii)), locale: formatValue(locale, makeRequired(isAscii)), - description: formatValue(description, makeRequired(isAscii)), + description: formatValue(description, makeRequired(helpers.identity)), email: formatValue(email, makeRequired(isEmail)), url: formatValue(formatUrl(url), isValidURL), logo: formatValue(formatUrl(logo), isValidURL), From ffd265d93acb8e1de37e1b77d2fa69dc9d0e6579 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 15:27:29 -0700 Subject: [PATCH 53/55] Preserve white space --- common/app/routes/Jobs/components/ShowJob.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index 42a0d29286..eb8bab36e0 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -80,6 +80,7 @@ export default React.createClass({

{ description }

From 9637c8739a911a7fa6e2bca528c98c98b8669738 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 15:43:50 -0700 Subject: [PATCH 54/55] Fix bold font issues --- client/less/main.less | 3 ++- common/app/routes/Jobs/components/ShowJob.jsx | 21 +++++++++++------- public/css/lato.css | 5 +++++ public/fonts/Lato-Bold.ttf | Bin 0 -> 121788 bytes 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100755 public/fonts/Lato-Bold.ttf diff --git a/client/less/main.less b/client/less/main.less index 4a86571c99..94c0e1d77a 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -10,7 +10,8 @@ html,body,div,span,a,li,td,th { } bold { - font-weight: 500; + font-family: 'Lato-Bold', sans-serif; + font-weight: Bold; } li, .wrappable { diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index eb8bab36e0..d7076f0a5e 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import { Row, Col, Thumbnail, Panel } from 'react-bootstrap'; +import { Well, Row, Col, Thumbnail, Panel } from 'react-bootstrap'; const defaultImage = 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png'; @@ -85,13 +85,18 @@ export default React.createClass({

{ description }

- - - How do I apply? { howToApply } - - + + + + How do I apply? +
+
+ { howToApply } + +
+
diff --git a/public/css/lato.css b/public/css/lato.css index 57941353cc..149eab749f 100644 --- a/public/css/lato.css +++ b/public/css/lato.css @@ -7,3 +7,8 @@ font-family: "Lato Light"; src: url(/fonts/Lato-Light.ttf) format("truetype"); } + +@font-face { + font-family: "Lato Bold"; + src: url(/fonts/Lato-Bold.ttf) format("truetype"); +} diff --git a/public/fonts/Lato-Bold.ttf b/public/fonts/Lato-Bold.ttf new file mode 100755 index 0000000000000000000000000000000000000000..74343694e2b2114272f38b1124813b972cb592e5 GIT binary patch literal 121788 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 literal 0 HcmV?d00001 From 6aa19ef24755cbd5250590dcf83880ec20a17b6f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 30 Oct 2015 16:58:49 -0700 Subject: [PATCH 55/55] Add a tag to links --- common/app/routes/Jobs/components/ShowJob.jsx | 14 ++++++++++++-- package.json | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx index d7076f0a5e..658a8f6295 100644 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -1,6 +1,8 @@ import React, { PropTypes } from 'react'; import { Well, Row, Col, Thumbnail, Panel } from 'react-bootstrap'; +import urlRegexFactory from 'url-regex'; +const urlRegex = urlRegexFactory(); const defaultImage = 'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png'; @@ -10,6 +12,12 @@ const thumbnailStyle = { maxWidth: '100px' }; +function addATags(text) { + return text.replace(urlRegex, function(match) { + return `${match}`; + }); +} + export default React.createClass({ displayName: 'ShowJob', propTypes: { @@ -90,10 +98,12 @@ export default React.createClass({ - How do I apply? + How do I apply?

- { howToApply } + diff --git a/package.json b/package.json index 10697a1778..38cdb03f2a 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "thundercats-react": "^0.3.0", "twit": "~1.1.20", "uglify-js": "~2.4.15", + "url-regex": "^3.0.0", "validator": "^3.22.1", "webpack": "^1.9.12", "xss-filters": "^1.2.6",