Jobs page initially renders

This commit is contained in:
Berkeley Martinez
2016-02-28 15:45:38 -08:00
parent 056d749ddd
commit 6bff10ea9c
6 changed files with 184 additions and 239 deletions

View File

@ -3,12 +3,17 @@ import { reducer as formReducer } from 'redux-form';
import { reducer as app } from './redux'; import { reducer as app } from './redux';
import { reducer as hikesApp } from './routes/Hikes/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 = {}) { export default function createReducer(sideReducers = {}) {
return combineReducers({ return combineReducers({
...sideReducers, ...sideReducers,
app, app,
hikesApp, hikesApp,
form: formReducer jobsApp,
form: formReducer.normalize(jobsNormalizer)
}); });
} }

View File

@ -15,12 +15,12 @@ import {
fetchJobs fetchJobs
} from '../redux/actions'; } from '../redux/actions';
const mapSateToProps = createSelector( const mapStateToProps = createSelector(
state => state.jobsApp.jobs.entities, state => state.jobsApp.jobs.entities,
state => state.jobsApp.jobs.results, state => state.jobsApp.jobs.results,
state => state.jobsApp, state => state.jobsApp,
(jobsMap, jobsById) => { (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( export default compose(
connect(mapSateToProps, bindableActions), connect(mapStateToProps, bindableActions),
contain(fetchOptions) contain(fetchOptions)
)(Jobs); )(Jobs);

View File

@ -1,17 +1,14 @@
import { helpers } from 'rx'; import { helpers } from 'rx';
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import { connector } from 'react-redux'; // import debug from 'debug';
import debug from 'debug';
import dedent from 'dedent'; import dedent from 'dedent';
import normalizeUrl from 'normalize-url';
import { getDefaults } from '../utils';
import { import {
inHTMLData, isAscii,
uriInSingleQuotedAttr isEmail,
} from 'xss-filters'; isURL
} from 'validator';
import { import {
Button, Button,
@ -20,30 +17,14 @@ import {
Row Row
} from 'react-bootstrap'; } from 'react-bootstrap';
import { import { saveJob } from '../redux/actions';
isAscii,
isEmail,
isURL
} from 'validator';
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 = ` const hightlightCopy = `
Highlight my post to make it stand out. (+$250) Highlight my post to make it stand out. (+$250)
`; `;
const isRemoteCopy = ` const isRemoteCopy = `
This job can be performed remotely. This job can be performed remotely.
`; `;
@ -60,177 +41,106 @@ const checkboxClass = dedent`
col-sm-6 col-md-offset-3 col-sm-6 col-md-offset-3
`; `;
function formatValue(value, validator, type = 'string') { const certTypes = {
const formatted = getDefaults(type); isFrontEndCert: 'isFrontEndCert',
if (validator && type === 'string' && typeof value === 'string') { isBackEndCert: 'isBackEndCert'
formatted.valid = validator(value);
}
if (value) {
formatted.value = value;
formatted.bsStyle = formatted.valid ? 'success' : 'error';
}
return formatted;
}
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;
}
function isValidURL(data) { function isValidURL(data) {
return isURL(data, { 'require_protocol': true }); 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) { function makeRequired(validator) {
return (val) => !!val && validator(val); return (val) => !!val && validator(val);
} }
const formOptions = { function validateForm(values) {
fields: [ return Object.keys(fieldValidators)
'position', .map(field => {
'locale', if (fieldValidators[field](values[field])) {
'description', return null;
'email', }
'url', return { [field]: fieldValidators[field](values[field]) };
'logo', })
'company', .filter(Boolean)
'isHighlighted', .reduce((errors, error) => ({ ...errors, ...error }), {});
'isRemoteOk', }
'isFrontEndCert',
'isBackEndCert', function getBsStyle(field) {
'howToApply' if (field.pristine) {
] return null;
}
return field.error ?
'error' :
'success';
} }
export class NewJob extends React.Component { export class NewJob extends React.Component {
static displayName = 'NewJob'; static displayName = 'NewJob';
static propTypes = { static propTypes = {
jobActions: PropTypes.object,
fields: 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() { componentDidMount() {
const { jobActions } = this.props; // this.prop.getSavedForm();
jobActions.getSavedForm(); }
},
handleChange(name, { target: { value } }) {
const { jobActions: { handleForm } } = this.props;
handleForm({ [name]: value });
},
handleCertClick(name) { handleCertClick(name) {
const { jobActions: { handleForm } } = this.props; const { fields } = this.props;
const otherButton = name === 'isFrontEndCert' ? Object.keys(certTypes).forEach(certType => {
'isBackEndCert' : if (certType === name) {
'isFrontEndCert'; return fields[certType].onChange(true);
}
handleForm({ fields[certType].onChange(false);
[name]: true,
[otherButton]: false
}); });
}, }
render() { render() {
const { const {
position, fields: {
locale, position,
description, locale,
email, description,
url, email,
logo, url,
company, logo,
isHighlighted, company,
isRemoteOk, isHighlighted,
howToApply, isRemoteOk,
isFrontEndCert, howToApply,
isBackEndCert, isFrontEndCert,
jobActions: { handleForm } isBackEndCert
},
handleSubmit
} = this.props; } = this.props;
const { handleChange } = this; const { handleChange } = this;
@ -246,7 +156,7 @@ export class NewJob extends React.Component {
<div className='text-center'> <div className='text-center'>
<form <form
className='form-horizontal' className='form-horizontal'
onSubmit={ this.handleSubmit }> onSubmit={ handleSubmit(data => this.handleSubmit(data)) }>
<div className='spacer'> <div className='spacer'>
<h2>First, select your ideal applicant: </h2> <h2>First, select your ideal applicant: </h2>
@ -259,10 +169,10 @@ export class NewJob extends React.Component {
<Row> <Row>
<Button <Button
bsStyle='primary' bsStyle='primary'
className={ isFrontEndCert ? 'active' : '' } className={ isFrontEndCert.value ? 'active' : '' }
onClick={ () => { onClick={ () => {
if (!isFrontEndCert) { if (!isFrontEndCert.value) {
this.handleCertClick('isFrontEndCert'); this.handleCertClick(certTypes.isFrontEndCert);
} }
}}> }}>
<h4>Front End Development Certified</h4> <h4>Front End Development Certified</h4>
@ -278,10 +188,10 @@ export class NewJob extends React.Component {
<Row> <Row>
<Button <Button
bsStyle='primary' bsStyle='primary'
className={ isBackEndCert ? 'active' : ''} className={ isBackEndCert.value ? 'active' : ''}
onClick={ () => { onClick={ () => {
if (!isBackEndCert) { if (!isBackEndCert.value) {
this.handleCertClick('isBackEndCert'); this.handleCertClick(certTypes.isBackEndCert);
} }
}}> }}>
<h4>Back End Development Certified</h4> <h4>Back End Development Certified</h4>
@ -300,47 +210,43 @@ export class NewJob extends React.Component {
</div> </div>
<hr /> <hr />
<Input <Input
bsStyle={ position.bsStyle } bsStyle={ getBsStyle(position) }
label='Job Title' label='Job Title'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('position', e) }
placeholder={ placeholder={
'e.g. Full Stack Developer, Front End Developer, etc.' 'e.g. Full Stack Developer, Front End Developer, etc.'
} }
required={ true } required={ true }
type='text' type='text'
value={ position.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...position }
/>
<Input <Input
bsStyle={ locale.bsStyle } bsStyle={ getBsStyle(locale) }
label='Location' label='Location'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('locale', e) }
placeholder='e.g. San Francisco, Remote, etc.' placeholder='e.g. San Francisco, Remote, etc.'
required={ true } required={ true }
type='text' type='text'
value={ locale.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...locale }
/>
<Input <Input
bsStyle={ description.bsStyle } bsStyle={ getBsStyle(description) }
label='Description' label='Description'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('description', e) }
required={ true } required={ true }
rows='10' rows='10'
type='textarea' type='textarea'
value={ description.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...description }
/>
<Input <Input
checked={ isRemoteOk.value }
label={ isRemoteCopy } label={ isRemoteCopy }
onChange={
({ target: { checked } }) => handleForm({
isRemoteOk: !!checked
})
}
type='checkbox' type='checkbox'
wrapperClassName={ checkboxClass } /> wrapperClassName={ checkboxClass }
{ ...isRemoteOk }
/>
<div className='spacer' /> <div className='spacer' />
<hr /> <hr />
@ -349,16 +255,16 @@ export class NewJob extends React.Component {
<h2>How should they apply?</h2> <h2>How should they apply?</h2>
</div> </div>
<Input <Input
bsStyle={ howToApply.bsStyle } bsStyle={ getBsStyle(howToApply) }
label=' ' label=' '
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('howToApply', e) }
placeholder={ howToApplyCopy } placeholder={ howToApplyCopy }
required={ true } required={ true }
rows='2' rows='2'
type='textarea' type='textarea'
value={ howToApply.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...howToApply }
/>
</Row> </Row>
<div className='spacer' /> <div className='spacer' />
@ -367,41 +273,42 @@ export class NewJob extends React.Component {
<h2>Tell us about your organization</h2> <h2>Tell us about your organization</h2>
</div> </div>
<Input <Input
bsStyle={ company.bsStyle } bsStyle={ getBsStyle(company) }
label='Company Name' label='Company Name'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('company', e) } onChange={ (e) => handleChange('company', e) }
type='text' type='text'
value={ company.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...company }
/>
<Input <Input
bsStyle={ email.bsStyle } bsStyle={ getBsStyle(email) }
label='Email' label='Email'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('email', e) }
placeholder='This is how we will contact you' placeholder='This is how we will contact you'
required={ true } required={ true }
type='email' type='email'
value={ email.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...email }
/>
<Input <Input
bsStyle={ url.bsStyle } bsStyle={ getBsStyle(url) }
label='URL' label='URL'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('url', e) }
placeholder='http://yourcompany.com' placeholder='http://yourcompany.com'
type='url' type='url'
value={ url.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...url }
/>
<Input <Input
bsStyle={ logo.bsStyle } bsStyle={ getBsStyle(logo) }
label='Logo' label='Logo'
labelClassName={ labelClass } labelClassName={ labelClass }
onChange={ (e) => handleChange('logo', e) }
placeholder='http://yourcompany.com/logo.png' placeholder='http://yourcompany.com/logo.png'
type='url' type='url'
value={ logo.value } wrapperClassName={ inputClass }
wrapperClassName={ inputClass } /> { ...logo }
/>
<div className='spacer' /> <div className='spacer' />
<hr /> <hr />
@ -416,7 +323,7 @@ export class NewJob extends React.Component {
mdOffset={ 3 }> mdOffset={ 3 }>
Highlight this ad to give it extra attention. Highlight this ad to give it extra attention.
<br /> <br />
Featured listings receive more clicks and more applications. Featured listings receive more clicks and more applications.
</Col> </Col>
</Row> </Row>
<div className='spacer' /> <div className='spacer' />
@ -424,17 +331,13 @@ export class NewJob extends React.Component {
<Input <Input
bsSize='large' bsSize='large'
bsStyle='success' bsStyle='success'
checked={ isHighlighted.value }
label={ hightlightCopy } label={ hightlightCopy }
onChange={
({ target: { checked } }) => handleForm({
isHighlighted: !!checked
})
}
type='checkbox' type='checkbox'
wrapperClassName={ wrapperClassName={
checkboxClass.replace('text-left', '') checkboxClass.replace('text-left', '')
} /> }
{ ...isHighlighted }
/>
</Row> </Row>
</div> </div>
@ -462,7 +365,13 @@ export class NewJob extends React.Component {
} }
export default reduxForm( export default reduxForm(
formOptions, {
mapStateToProps, form: 'NewJob',
bindableActions fields,
validate: validateForm
},
null,
{
onSubmit: saveJob
}
)(NewJob); )(NewJob);

View File

@ -1,7 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Button, Row, Col } from 'react-bootstrap'; import { Button, Row, Col } from 'react-bootstrap';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
import { goBack, push } from 'react-router-redux'; import { goBack, push } from 'react-router-redux';
@ -10,11 +9,7 @@ import JobNotFound from './JobNotFound.jsx';
import { clearSavedForm, saveJobToDb } from '../redux/actions'; import { clearSavedForm, saveJobToDb } from '../redux/actions';
const mapStateToProps = createSelector( const mapStateToProps = state => ({ job: state.jobsApp.newJob });
state => state.jobsApp.previewJob,
state => state.jobsApp.jobs.entities
(job, jobsMap) => ({ job: jobsMap[job] || {} })
);
const bindableActions = { const bindableActions = {
goBack, goBack,

View File

@ -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
}
};

View File

@ -66,8 +66,6 @@ export default function contain(options = {}, Component) {
} }
static displayName = `Container(${Component.displayName})`; static displayName = `Container(${Component.displayName})`;
static propTypes = Component.propTypes;
static contextTypes = { static contextTypes = {
...Component.contextTypes, ...Component.contextTypes,
professor: PropTypes.object professor: PropTypes.object