From 6bff10ea9c7d50f726ad147713935ad8bb02b62c Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 28 Feb 2016 15:45:38 -0800 Subject: [PATCH] Jobs page initially renders --- common/app/create-reducer.js | 7 +- common/app/routes/Jobs/components/Jobs.jsx | 6 +- common/app/routes/Jobs/components/NewJob.jsx | 363 +++++++----------- common/app/routes/Jobs/components/Preview.jsx | 7 +- .../routes/Jobs/redux/jobs-form-normalizer.js | 38 ++ common/app/utils/professor-x.js | 2 - 6 files changed, 184 insertions(+), 239 deletions(-) create mode 100644 common/app/routes/Jobs/redux/jobs-form-normalizer.js diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js index 82d51d9546..2e9ac1dba9 100644 --- a/common/app/create-reducer.js +++ b/common/app/create-reducer.js @@ -3,12 +3,17 @@ import { reducer as formReducer } from 'redux-form'; import { reducer as app } from './redux'; import { reducer as hikesApp } from './routes/Hikes/redux'; +import { + reducer as jobsApp, + formNormalizer as jobsNormalizer +} from './routes/Jobs/redux'; export default function createReducer(sideReducers = {}) { return combineReducers({ ...sideReducers, app, hikesApp, - form: formReducer + jobsApp, + form: formReducer.normalize(jobsNormalizer) }); } diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index bea7d205b0..36fe14cbe4 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -15,12 +15,12 @@ import { fetchJobs } from '../redux/actions'; -const mapSateToProps = createSelector( +const mapStateToProps = createSelector( state => state.jobsApp.jobs.entities, state => state.jobsApp.jobs.results, state => state.jobsApp, (jobsMap, jobsById) => { - return jobsById.map(id => jobsMap[id]); + return { jobs: jobsById.map(id => jobsMap[id]) }; } ); @@ -152,6 +152,6 @@ export class Jobs extends PureComponent { } export default compose( - connect(mapSateToProps, bindableActions), + connect(mapStateToProps, bindableActions), contain(fetchOptions) )(Jobs); diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 1111057def..3fbccf4547 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,17 +1,14 @@ import { helpers } from 'rx'; import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; -import { connector } from 'react-redux'; -import debug from 'debug'; +// import debug from 'debug'; import dedent from 'dedent'; -import normalizeUrl from 'normalize-url'; - -import { getDefaults } from '../utils'; import { - inHTMLData, - uriInSingleQuotedAttr -} from 'xss-filters'; + isAscii, + isEmail, + isURL +} from 'validator'; import { Button, @@ -20,30 +17,14 @@ import { Row } from 'react-bootstrap'; -import { - isAscii, - isEmail, - isURL -} from 'validator'; +import { saveJob } from '../redux/actions'; -const log = debug('fcc:jobs:newForm'); +// const log = debug('fcc:jobs:newForm'); -const checkValidity = [ - 'position', - 'locale', - 'description', - 'email', - 'url', - 'logo', - 'company', - 'isHighlighted', - 'howToApply' -]; const hightlightCopy = ` Highlight my post to make it stand out. (+$250) `; - const isRemoteCopy = ` This job can be performed remotely. `; @@ -60,177 +41,106 @@ const checkboxClass = dedent` col-sm-6 col-md-offset-3 `; -function formatValue(value, validator, type = 'string') { - const formatted = getDefaults(type); - if (validator && type === 'string' && typeof value === 'string') { - formatted.valid = validator(value); - } - if (value) { - formatted.value = value; - formatted.bsStyle = formatted.valid ? 'success' : 'error'; - } - return formatted; -} - -const normalizeOptions = { - stripWWW: false +const certTypes = { + isFrontEndCert: 'isFrontEndCert', + isBackEndCert: 'isBackEndCert' }; -function formatUrl(url, shouldKeepTrailingSlash = true) { - if ( - typeof url === 'string' && - url.length > 4 && - url.indexOf('.') !== -1 - ) { - // prevent trailing / from being stripped during typing - let lastChar = ''; - if (shouldKeepTrailingSlash && url.substring(url.length - 1) === '/') { - lastChar = '/'; - } - return normalizeUrl(url, normalizeOptions) + lastChar; - } - return url; -} - function isValidURL(data) { return isURL(data, { 'require_protocol': true }); } +const fields = [ + 'position', + 'locale', + 'description', + 'email', + 'url', + 'logo', + 'company', + 'isHighlighted', + 'isRemoteOk', + 'isFrontEndCert', + 'isBackEndCert', + 'howToApply' +]; + +const fieldValidators = { + position: makeRequired(isAscii), + locale: makeRequired(isAscii), + description: makeRequired(helpers.identity), + email: makeRequired(isEmail), + url: isValidURL, + logo: isValidURL, + company: makeRequired(isAscii), + howToApply: makeRequired(isAscii) +}; + function makeRequired(validator) { return (val) => !!val && validator(val); } -const formOptions = { - fields: [ - 'position', - 'locale', - 'description', - 'email', - 'url', - 'logo', - 'company', - 'isHighlighted', - 'isRemoteOk', - 'isFrontEndCert', - 'isBackEndCert', - 'howToApply' - ] +function validateForm(values) { + return Object.keys(fieldValidators) + .map(field => { + if (fieldValidators[field](values[field])) { + return null; + } + return { [field]: fieldValidators[field](values[field]) }; + }) + .filter(Boolean) + .reduce((errors, error) => ({ ...errors, ...error }), {}); +} + +function getBsStyle(field) { + if (field.pristine) { + return null; + } + + return field.error ? + 'error' : + 'success'; } export class NewJob extends React.Component { static displayName = 'NewJob'; static propTypes = { - jobActions: PropTypes.object, fields: PropTypes.object, - onSubmit: PropTypes.func + handleSubmit: PropTypes.func }; - handleSubmit(e) { - e.preventDefault(); - const pros = this.props; - let valid = true; - checkValidity.forEach((prop) => { - // if value exist, check if it is valid - if (pros[prop].value && pros[prop].type !== 'boolean') { - valid = valid && !!pros[prop].valid; - } - }); - - if ( - !valid || - !pros.isFrontEndCert && - !pros.isBackEndCert - ) { - debug('form not valid'); - return; - } - - const { - jobActions, - - // form values - position, - locale, - description, - email, - url, - logo, - company, - isFrontEndCert, - isBackEndCert, - isHighlighted, - isRemoteOk, - howToApply - } = this.props; - - // sanitize user output - const jobValues = { - position: inHTMLData(position.value), - locale: inHTMLData(locale.value), - description: inHTMLData(description.value), - email: inHTMLData(email.value), - url: formatUrl(uriInSingleQuotedAttr(url.value), false), - logo: formatUrl(uriInSingleQuotedAttr(logo.value), false), - company: inHTMLData(company.value), - isHighlighted: !!isHighlighted.value, - isRemoteOk: !!isRemoteOk.value, - howToApply: inHTMLData(howToApply.value), - isFrontEndCert, - isBackEndCert - }; - - const job = Object.keys(jobValues).reduce((accu, prop) => { - if (jobValues[prop]) { - accu[prop] = jobValues[prop]; - } - return accu; - }, {}); - - job.postedOn = new Date(); - debug('job sanitized', job); - jobActions.saveForm(job); - - this.history.pushState(null, '/jobs/new/preview'); - }, - componentDidMount() { - const { jobActions } = this.props; - jobActions.getSavedForm(); - }, - - handleChange(name, { target: { value } }) { - const { jobActions: { handleForm } } = this.props; - handleForm({ [name]: value }); - }, + // this.prop.getSavedForm(); + } handleCertClick(name) { - const { jobActions: { handleForm } } = this.props; - const otherButton = name === 'isFrontEndCert' ? - 'isBackEndCert' : - 'isFrontEndCert'; - - handleForm({ - [name]: true, - [otherButton]: false + const { fields } = this.props; + Object.keys(certTypes).forEach(certType => { + if (certType === name) { + return fields[certType].onChange(true); + } + fields[certType].onChange(false); }); - }, + } render() { const { - position, - locale, - description, - email, - url, - logo, - company, - isHighlighted, - isRemoteOk, - howToApply, - isFrontEndCert, - isBackEndCert, - jobActions: { handleForm } + fields: { + position, + locale, + description, + email, + url, + logo, + company, + isHighlighted, + isRemoteOk, + howToApply, + isFrontEndCert, + isBackEndCert + }, + handleSubmit } = this.props; const { handleChange } = this; @@ -246,7 +156,7 @@ export class NewJob extends React.Component {
+ onSubmit={ handleSubmit(data => this.handleSubmit(data)) }>

First, select your ideal applicant:

@@ -259,10 +169,10 @@ export class NewJob extends React.Component {

handleChange('position', e) } placeholder={ 'e.g. Full Stack Developer, Front End Developer, etc.' } required={ true } type='text' - value={ position.value } - wrapperClassName={ inputClass } /> + wrapperClassName={ inputClass } + { ...position } + /> handleChange('locale', e) } placeholder='e.g. San Francisco, Remote, etc.' required={ true } type='text' - value={ locale.value } - wrapperClassName={ inputClass } /> + wrapperClassName={ inputClass } + { ...locale } + /> handleChange('description', e) } required={ true } rows='10' type='textarea' - value={ description.value } - wrapperClassName={ inputClass } /> + wrapperClassName={ inputClass } + { ...description } + /> handleForm({ - isRemoteOk: !!checked - }) - } type='checkbox' - wrapperClassName={ checkboxClass } /> + wrapperClassName={ checkboxClass } + { ...isRemoteOk } + />

@@ -349,16 +255,16 @@ export class NewJob extends React.Component {

How should they apply?

handleChange('howToApply', e) } placeholder={ howToApplyCopy } required={ true } rows='2' type='textarea' - value={ howToApply.value } - wrapperClassName={ inputClass } /> + wrapperClassName={ inputClass } + { ...howToApply } + />
@@ -367,41 +273,42 @@ export class NewJob extends React.Component {

Tell us about your organization

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

@@ -416,7 +323,7 @@ export class NewJob extends React.Component { mdOffset={ 3 }> Highlight this ad to give it extra attention.
- Featured listings receive more clicks and more applications. + Featured listings receive more clicks and more applications.
@@ -424,17 +331,13 @@ export class NewJob extends React.Component { handleForm({ - isHighlighted: !!checked - }) - } type='checkbox' wrapperClassName={ checkboxClass.replace('text-left', '') - } /> + } + { ...isHighlighted } + />
@@ -462,7 +365,13 @@ export class NewJob extends React.Component { } export default reduxForm( - formOptions, - mapStateToProps, - bindableActions + { + form: 'NewJob', + fields, + validate: validateForm + }, + null, + { + onSubmit: saveJob + } )(NewJob); diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index cfdec61ddb..e7eca1b92a 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,7 +1,6 @@ import React, { PropTypes } from 'react'; import { Button, Row, Col } from 'react-bootstrap'; import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; import PureComponent from 'react-pure-render/component'; import { goBack, push } from 'react-router-redux'; @@ -10,11 +9,7 @@ import JobNotFound from './JobNotFound.jsx'; import { clearSavedForm, saveJobToDb } from '../redux/actions'; -const mapStateToProps = createSelector( - state => state.jobsApp.previewJob, - state => state.jobsApp.jobs.entities - (job, jobsMap) => ({ job: jobsMap[job] || {} }) -); +const mapStateToProps = state => ({ job: state.jobsApp.newJob }); const bindableActions = { goBack, diff --git a/common/app/routes/Jobs/redux/jobs-form-normalizer.js b/common/app/routes/Jobs/redux/jobs-form-normalizer.js new file mode 100644 index 0000000000..06c5ffa2d7 --- /dev/null +++ b/common/app/routes/Jobs/redux/jobs-form-normalizer.js @@ -0,0 +1,38 @@ +import normalizeUrl from 'normalize-url'; +import { + inHTMLData, + uriInSingleQuotedAttr +} from 'xss-filters'; + +const normalizeOptions = { + stripWWW: false +}; + +function formatUrl(url, shouldKeepTrailingSlash = true) { + if ( + typeof url === 'string' && + url.length > 4 && + url.indexOf('.') !== -1 + ) { + // prevent trailing / from being stripped during typing + let lastChar = ''; + if (shouldKeepTrailingSlash && url.substring(url.length - 1) === '/') { + lastChar = '/'; + } + return normalizeUrl(url, normalizeOptions) + lastChar; + } + return url; +} + +export default { + NewJob: { + position: inHTMLData, + locale: inHTMLData, + description: inHTMLData, + email: inHTMLData, + url: value => formatUrl(uriInSingleQuotedAttr(value)), + logo: value => formatUrl(uriInSingleQuotedAttr(value)), + company: inHTMLData, + howToApply: inHTMLData + } +}; diff --git a/common/app/utils/professor-x.js b/common/app/utils/professor-x.js index d1b2512858..194b28178e 100644 --- a/common/app/utils/professor-x.js +++ b/common/app/utils/professor-x.js @@ -66,8 +66,6 @@ export default function contain(options = {}, Component) { } static displayName = `Container(${Component.displayName})`; - static propTypes = Component.propTypes; - static contextTypes = { ...Component.contextTypes, professor: PropTypes.object