Wrap up flux actions
This commit is contained in:
67
client/sagas/local-storage-saga.js
Normal file
67
client/sagas/local-storage-saga.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
saveForm,
|
||||||
|
clearForm,
|
||||||
|
loadSavedForm
|
||||||
|
} from '../common/app/routes/Jobs/redux/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadSavedFormCompleted
|
||||||
|
} from '../common/app/routes/Jobs/redux/actions';
|
||||||
|
|
||||||
|
const formKey = 'newJob';
|
||||||
|
let enabled = false;
|
||||||
|
let store = typeof window !== 'undefined' ?
|
||||||
|
window.localStorage :
|
||||||
|
false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const testKey = '__testKey__';
|
||||||
|
store.setItem(testKey, testKey);
|
||||||
|
enabled = store.getItem(testKey) !== testKey;
|
||||||
|
store.removeItem(testKey);
|
||||||
|
} catch (e) {
|
||||||
|
enabled = !e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
console.error(new Error('No localStorage found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => ({ dispatch }) => next => {
|
||||||
|
return function localStorageSaga(action) {
|
||||||
|
if (!enabled) { return next(action); }
|
||||||
|
|
||||||
|
if (action.type === saveForm) {
|
||||||
|
const form = action.payload;
|
||||||
|
try {
|
||||||
|
store.setItem(formKey, JSON.stringify(form));
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return dispatch({
|
||||||
|
type: 'app.handleError',
|
||||||
|
error: new Error('could not parse form data')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === clearForm) {
|
||||||
|
store.removeItem(formKey);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === loadSavedForm) {
|
||||||
|
const formString = store.getItem(formKey);
|
||||||
|
try {
|
||||||
|
const form = JSON.parse(formString);
|
||||||
|
return dispatch(loadSavedFormCompleted(form));
|
||||||
|
} catch (err) {
|
||||||
|
return dispatch({
|
||||||
|
type: 'app.handleError',
|
||||||
|
error: new Error('could not parse form data')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
};
|
@ -21,5 +21,7 @@ const types = [
|
|||||||
'goToNextHike'
|
'goToNextHike'
|
||||||
];
|
];
|
||||||
|
|
||||||
export default types
|
export default types.reduce((types, type) => {
|
||||||
.reduce((types, type) => ({ ...types, [type]: `videos.${type}` }), {});
|
types[type] = `videos.${type}`;
|
||||||
|
return types;
|
||||||
|
}, {});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { cloneElement, PropTypes } from 'react';
|
import React, { cloneElement, PropTypes } from 'react';
|
||||||
import { connect, compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { push } from 'react-router-redux';
|
import { push } from 'react-router-redux';
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { helpers } from 'rx';
|
import { helpers } from 'rx';
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { History } from 'react-router';
|
import { reduxForm } from 'redux-form';
|
||||||
import { contain } from 'thundercats-react';
|
import { connector } from 'react-redux';
|
||||||
import debugFactory from 'debug';
|
import debug from 'debug';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
import normalizeUrl from 'normalize-url';
|
import normalizeUrl from 'normalize-url';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ import {
|
|||||||
isURL
|
isURL
|
||||||
} from 'validator';
|
} from 'validator';
|
||||||
|
|
||||||
const debug = debugFactory('fcc:jobs:newForm');
|
const log = debug('fcc:jobs:newForm');
|
||||||
|
|
||||||
const checkValidity = [
|
const checkValidity = [
|
||||||
'position',
|
'position',
|
||||||
@ -100,396 +100,369 @@ function makeRequired(validator) {
|
|||||||
return (val) => !!val && validator(val);
|
return (val) => !!val && validator(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contain({
|
const formOptions = {
|
||||||
store: 'appStore',
|
fields: [
|
||||||
actions: 'jobActions',
|
'position',
|
||||||
map({ jobsApp: { form = {} } }) {
|
'locale',
|
||||||
const {
|
'description',
|
||||||
position,
|
'email',
|
||||||
locale,
|
'url',
|
||||||
description,
|
'logo',
|
||||||
email,
|
'company',
|
||||||
url,
|
'isHighlighted',
|
||||||
logo,
|
'isRemoteOk',
|
||||||
company,
|
'isFrontEndCert',
|
||||||
isFrontEndCert = true,
|
'isBackEndCert',
|
||||||
isBackEndCert,
|
'howToApply'
|
||||||
isHighlighted,
|
]
|
||||||
isRemoteOk,
|
}
|
||||||
howToApply
|
|
||||||
} = form;
|
|
||||||
return {
|
|
||||||
position: formatValue(position, makeRequired(isAscii)),
|
|
||||||
locale: formatValue(locale, makeRequired(isAscii)),
|
|
||||||
description: formatValue(description, makeRequired(helpers.identity)),
|
|
||||||
email: formatValue(email, makeRequired(isEmail)),
|
|
||||||
url: formatValue(formatUrl(url), isValidURL),
|
|
||||||
logo: formatValue(formatUrl(logo), isValidURL),
|
|
||||||
company: formatValue(company, makeRequired(isAscii)),
|
|
||||||
isHighlighted: formatValue(isHighlighted, null, 'bool'),
|
|
||||||
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
|
||||||
howToApply: formatValue(howToApply, makeRequired(isAscii)),
|
|
||||||
isFrontEndCert,
|
|
||||||
isBackEndCert
|
|
||||||
};
|
|
||||||
},
|
|
||||||
subscribeOnWillMount() {
|
|
||||||
return typeof window !== 'undefined';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
React.createClass({
|
|
||||||
displayName: 'NewJob',
|
|
||||||
|
|
||||||
propTypes: {
|
export class NewJob extends React.Component {
|
||||||
jobActions: PropTypes.object,
|
static displayName = 'NewJob';
|
||||||
position: PropTypes.object,
|
|
||||||
locale: PropTypes.object,
|
|
||||||
description: PropTypes.object,
|
|
||||||
email: PropTypes.object,
|
|
||||||
url: PropTypes.object,
|
|
||||||
logo: PropTypes.object,
|
|
||||||
company: PropTypes.object,
|
|
||||||
isHighlighted: PropTypes.object,
|
|
||||||
isRemoteOk: PropTypes.object,
|
|
||||||
isFrontEndCert: PropTypes.bool,
|
|
||||||
isBackEndCert: PropTypes.bool,
|
|
||||||
howToApply: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [History],
|
static propTypes = {
|
||||||
|
jobActions: PropTypes.object,
|
||||||
|
fields: PropTypes.object,
|
||||||
|
onSubmit: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
handleSubmit(e) {
|
handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const pros = this.props;
|
const pros = this.props;
|
||||||
let valid = true;
|
let valid = true;
|
||||||
checkValidity.forEach((prop) => {
|
checkValidity.forEach((prop) => {
|
||||||
// if value exist, check if it is valid
|
// if value exist, check if it is valid
|
||||||
if (pros[prop].value && pros[prop].type !== 'boolean') {
|
if (pros[prop].value && pros[prop].type !== 'boolean') {
|
||||||
valid = valid && !!pros[prop].valid;
|
valid = valid && !!pros[prop].valid;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!valid ||
|
|
||||||
!pros.isFrontEndCert &&
|
|
||||||
!pros.isBackEndCert
|
|
||||||
) {
|
|
||||||
debug('form not valid');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
if (
|
||||||
jobActions,
|
!valid ||
|
||||||
|
!pros.isFrontEndCert &&
|
||||||
|
!pros.isBackEndCert
|
||||||
|
) {
|
||||||
|
debug('form not valid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// form values
|
const {
|
||||||
position,
|
jobActions,
|
||||||
locale,
|
|
||||||
description,
|
|
||||||
email,
|
|
||||||
url,
|
|
||||||
logo,
|
|
||||||
company,
|
|
||||||
isFrontEndCert,
|
|
||||||
isBackEndCert,
|
|
||||||
isHighlighted,
|
|
||||||
isRemoteOk,
|
|
||||||
howToApply
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// sanitize user output
|
// form values
|
||||||
const jobValues = {
|
position,
|
||||||
position: inHTMLData(position.value),
|
locale,
|
||||||
locale: inHTMLData(locale.value),
|
description,
|
||||||
description: inHTMLData(description.value),
|
email,
|
||||||
email: inHTMLData(email.value),
|
url,
|
||||||
url: formatUrl(uriInSingleQuotedAttr(url.value), false),
|
logo,
|
||||||
logo: formatUrl(uriInSingleQuotedAttr(logo.value), false),
|
company,
|
||||||
company: inHTMLData(company.value),
|
isFrontEndCert,
|
||||||
isHighlighted: !!isHighlighted.value,
|
isBackEndCert,
|
||||||
isRemoteOk: !!isRemoteOk.value,
|
isHighlighted,
|
||||||
howToApply: inHTMLData(howToApply.value),
|
isRemoteOk,
|
||||||
isFrontEndCert,
|
howToApply
|
||||||
isBackEndCert
|
} = this.props;
|
||||||
};
|
|
||||||
|
|
||||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
// sanitize user output
|
||||||
if (jobValues[prop]) {
|
const jobValues = {
|
||||||
accu[prop] = jobValues[prop];
|
position: inHTMLData(position.value),
|
||||||
}
|
locale: inHTMLData(locale.value),
|
||||||
return accu;
|
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
|
||||||
|
};
|
||||||
|
|
||||||
job.postedOn = new Date();
|
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
||||||
debug('job sanitized', job);
|
if (jobValues[prop]) {
|
||||||
jobActions.saveForm(job);
|
accu[prop] = jobValues[prop];
|
||||||
|
}
|
||||||
|
return accu;
|
||||||
|
}, {});
|
||||||
|
|
||||||
this.history.pushState(null, '/jobs/new/preview');
|
job.postedOn = new Date();
|
||||||
},
|
debug('job sanitized', job);
|
||||||
|
jobActions.saveForm(job);
|
||||||
|
|
||||||
componentDidMount() {
|
this.history.pushState(null, '/jobs/new/preview');
|
||||||
const { jobActions } = this.props;
|
},
|
||||||
jobActions.getSavedForm();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChange(name, { target: { value } }) {
|
componentDidMount() {
|
||||||
const { jobActions: { handleForm } } = this.props;
|
const { jobActions } = this.props;
|
||||||
handleForm({ [name]: value });
|
jobActions.getSavedForm();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleCertClick(name) {
|
handleChange(name, { target: { value } }) {
|
||||||
const { jobActions: { handleForm } } = this.props;
|
const { jobActions: { handleForm } } = this.props;
|
||||||
const otherButton = name === 'isFrontEndCert' ?
|
handleForm({ [name]: value });
|
||||||
'isBackEndCert' :
|
},
|
||||||
'isFrontEndCert';
|
|
||||||
|
|
||||||
handleForm({
|
handleCertClick(name) {
|
||||||
[name]: true,
|
const { jobActions: { handleForm } } = this.props;
|
||||||
[otherButton]: false
|
const otherButton = name === 'isFrontEndCert' ?
|
||||||
});
|
'isBackEndCert' :
|
||||||
},
|
'isFrontEndCert';
|
||||||
|
|
||||||
render() {
|
handleForm({
|
||||||
const {
|
[name]: true,
|
||||||
position,
|
[otherButton]: false
|
||||||
locale,
|
});
|
||||||
description,
|
},
|
||||||
email,
|
|
||||||
url,
|
|
||||||
logo,
|
|
||||||
company,
|
|
||||||
isHighlighted,
|
|
||||||
isRemoteOk,
|
|
||||||
howToApply,
|
|
||||||
isFrontEndCert,
|
|
||||||
isBackEndCert,
|
|
||||||
jobActions: { handleForm }
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { handleChange } = this;
|
render() {
|
||||||
const labelClass = 'col-sm-offset-1 col-sm-2';
|
const {
|
||||||
const inputClass = 'col-sm-6';
|
position,
|
||||||
|
locale,
|
||||||
|
description,
|
||||||
|
email,
|
||||||
|
url,
|
||||||
|
logo,
|
||||||
|
company,
|
||||||
|
isHighlighted,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply,
|
||||||
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
|
jobActions: { handleForm }
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
const { handleChange } = this;
|
||||||
<div>
|
const labelClass = 'col-sm-offset-1 col-sm-2';
|
||||||
<Row>
|
const inputClass = 'col-sm-6';
|
||||||
<Col
|
|
||||||
md={ 10 }
|
|
||||||
mdOffset={ 1 }>
|
|
||||||
<div className='text-center'>
|
|
||||||
<form
|
|
||||||
className='form-horizontal'
|
|
||||||
onSubmit={ this.handleSubmit }>
|
|
||||||
|
|
||||||
<div className='spacer'>
|
return (
|
||||||
<h2>First, select your ideal applicant: </h2>
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }>
|
||||||
|
<div className='text-center'>
|
||||||
|
<form
|
||||||
|
className='form-horizontal'
|
||||||
|
onSubmit={ this.handleSubmit }>
|
||||||
|
|
||||||
|
<div className='spacer'>
|
||||||
|
<h2>First, select your ideal applicant: </h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
xs={ 6 }
|
||||||
|
xsOffset={ 3 }>
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
bsStyle='primary'
|
||||||
|
className={ isFrontEndCert ? 'active' : '' }
|
||||||
|
onClick={ () => {
|
||||||
|
if (!isFrontEndCert) {
|
||||||
|
this.handleCertClick('isFrontEndCert');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<h4>Front End Development Certified</h4>
|
||||||
|
You can expect each applicant
|
||||||
|
to have a code portfolio using the
|
||||||
|
following technologies:
|
||||||
|
HTML5, CSS, jQuery, API integrations
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<div className='button-spacer' />
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
bsStyle='primary'
|
||||||
|
className={ isBackEndCert ? 'active' : ''}
|
||||||
|
onClick={ () => {
|
||||||
|
if (!isBackEndCert) {
|
||||||
|
this.handleCertClick('isBackEndCert');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<h4>Back End Development Certified</h4>
|
||||||
|
You can expect each applicant to have a code
|
||||||
|
portfolio using the following technologies:
|
||||||
|
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
||||||
|
JavaScript, Node.js, MongoDB, Express.js
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer'>
|
||||||
|
<h2>Tell us about the position</h2>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<Input
|
||||||
|
bsStyle={ position.bsStyle }
|
||||||
|
label='Job Title'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('position', e) }
|
||||||
|
placeholder={
|
||||||
|
'e.g. Full Stack Developer, Front End Developer, etc.'
|
||||||
|
}
|
||||||
|
required={ true }
|
||||||
|
type='text'
|
||||||
|
value={ position.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ locale.bsStyle }
|
||||||
|
label='Location'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('locale', e) }
|
||||||
|
placeholder='e.g. San Francisco, Remote, etc.'
|
||||||
|
required={ true }
|
||||||
|
type='text'
|
||||||
|
value={ locale.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ description.bsStyle }
|
||||||
|
label='Description'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('description', e) }
|
||||||
|
required={ true }
|
||||||
|
rows='10'
|
||||||
|
type='textarea'
|
||||||
|
value={ description.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
checked={ isRemoteOk.value }
|
||||||
|
label={ isRemoteCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isRemoteOk: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={ checkboxClass } />
|
||||||
|
<div className='spacer' />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<Row>
|
||||||
|
<div>
|
||||||
|
<h2>How should they apply?</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ howToApply.bsStyle }
|
||||||
|
label=' '
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('howToApply', e) }
|
||||||
|
placeholder={ howToApplyCopy }
|
||||||
|
required={ true }
|
||||||
|
rows='2'
|
||||||
|
type='textarea'
|
||||||
|
value={ howToApply.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className='spacer' />
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<h2>Tell us about your organization</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ company.bsStyle }
|
||||||
|
label='Company Name'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('company', e) }
|
||||||
|
type='text'
|
||||||
|
value={ company.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ email.bsStyle }
|
||||||
|
label='Email'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('email', e) }
|
||||||
|
placeholder='This is how we will contact you'
|
||||||
|
required={ true }
|
||||||
|
type='email'
|
||||||
|
value={ email.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ url.bsStyle }
|
||||||
|
label='URL'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('url', e) }
|
||||||
|
placeholder='http://yourcompany.com'
|
||||||
|
type='url'
|
||||||
|
value={ url.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ logo.bsStyle }
|
||||||
|
label='Logo'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('logo', e) }
|
||||||
|
placeholder='http://yourcompany.com/logo.png'
|
||||||
|
type='url'
|
||||||
|
value={ logo.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
|
||||||
|
<div className='spacer' />
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h2>Make it stand out</h2>
|
||||||
|
</div>
|
||||||
|
<div className='spacer' />
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
xs={ 6 }
|
md={ 6 }
|
||||||
xsOffset={ 3 }>
|
mdOffset={ 3 }>
|
||||||
<Row>
|
|
||||||
<Button
|
|
||||||
bsStyle='primary'
|
|
||||||
className={ isFrontEndCert ? 'active' : '' }
|
|
||||||
onClick={ () => {
|
|
||||||
if (!isFrontEndCert) {
|
|
||||||
this.handleCertClick('isFrontEndCert');
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<h4>Front End Development Certified</h4>
|
|
||||||
You can expect each applicant
|
|
||||||
to have a code portfolio using the
|
|
||||||
following technologies:
|
|
||||||
HTML5, CSS, jQuery, API integrations
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
<div className='button-spacer' />
|
|
||||||
<Row>
|
|
||||||
<Button
|
|
||||||
bsStyle='primary'
|
|
||||||
className={ isBackEndCert ? 'active' : ''}
|
|
||||||
onClick={ () => {
|
|
||||||
if (!isBackEndCert) {
|
|
||||||
this.handleCertClick('isBackEndCert');
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<h4>Back End Development Certified</h4>
|
|
||||||
You can expect each applicant to have a code
|
|
||||||
portfolio using the following technologies:
|
|
||||||
HTML5, CSS, jQuery, API integrations, MVC Framework,
|
|
||||||
JavaScript, Node.js, MongoDB, Express.js
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer'>
|
|
||||||
<h2>Tell us about the position</h2>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<Input
|
|
||||||
bsStyle={ position.bsStyle }
|
|
||||||
label='Job Title'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('position', e) }
|
|
||||||
placeholder={
|
|
||||||
'e.g. Full Stack Developer, Front End Developer, etc.'
|
|
||||||
}
|
|
||||||
required={ true }
|
|
||||||
type='text'
|
|
||||||
value={ position.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
|
||||||
bsStyle={ locale.bsStyle }
|
|
||||||
label='Location'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('locale', e) }
|
|
||||||
placeholder='e.g. San Francisco, Remote, etc.'
|
|
||||||
required={ true }
|
|
||||||
type='text'
|
|
||||||
value={ locale.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
|
||||||
bsStyle={ description.bsStyle }
|
|
||||||
label='Description'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('description', e) }
|
|
||||||
required={ true }
|
|
||||||
rows='10'
|
|
||||||
type='textarea'
|
|
||||||
value={ description.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
|
||||||
checked={ isRemoteOk.value }
|
|
||||||
label={ isRemoteCopy }
|
|
||||||
onChange={
|
|
||||||
({ target: { checked } }) => handleForm({
|
|
||||||
isRemoteOk: !!checked
|
|
||||||
})
|
|
||||||
}
|
|
||||||
type='checkbox'
|
|
||||||
wrapperClassName={ checkboxClass } />
|
|
||||||
<div className='spacer' />
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
<Row>
|
|
||||||
<div>
|
|
||||||
<h2>How should they apply?</h2>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
bsStyle={ howToApply.bsStyle }
|
|
||||||
label=' '
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('howToApply', e) }
|
|
||||||
placeholder={ howToApplyCopy }
|
|
||||||
required={ true }
|
|
||||||
rows='2'
|
|
||||||
type='textarea'
|
|
||||||
value={ howToApply.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<div className='spacer' />
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<h2>Tell us about your organization</h2>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
bsStyle={ company.bsStyle }
|
|
||||||
label='Company Name'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('company', e) }
|
|
||||||
type='text'
|
|
||||||
value={ company.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
|
||||||
bsStyle={ email.bsStyle }
|
|
||||||
label='Email'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('email', e) }
|
|
||||||
placeholder='This is how we will contact you'
|
|
||||||
required={ true }
|
|
||||||
type='email'
|
|
||||||
value={ email.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
|
||||||
bsStyle={ url.bsStyle }
|
|
||||||
label='URL'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('url', e) }
|
|
||||||
placeholder='http://yourcompany.com'
|
|
||||||
type='url'
|
|
||||||
value={ url.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
<Input
|
|
||||||
bsStyle={ logo.bsStyle }
|
|
||||||
label='Logo'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('logo', e) }
|
|
||||||
placeholder='http://yourcompany.com/logo.png'
|
|
||||||
type='url'
|
|
||||||
value={ logo.value }
|
|
||||||
wrapperClassName={ inputClass } />
|
|
||||||
|
|
||||||
<div className='spacer' />
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h2>Make it stand out</h2>
|
|
||||||
</div>
|
|
||||||
<div className='spacer' />
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
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>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<Row>
|
|
||||||
<Input
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='success'
|
|
||||||
checked={ isHighlighted.value }
|
|
||||||
label={ hightlightCopy }
|
|
||||||
onChange={
|
|
||||||
({ target: { checked } }) => handleForm({
|
|
||||||
isHighlighted: !!checked
|
|
||||||
})
|
|
||||||
}
|
|
||||||
type='checkbox'
|
|
||||||
wrapperClassName={
|
|
||||||
checkboxClass.replace('text-left', '')
|
|
||||||
} />
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
className='text-left'
|
|
||||||
lg={ 6 }
|
|
||||||
lgOffset={ 3 }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='primary'
|
|
||||||
type='submit'>
|
|
||||||
Preview My Ad
|
|
||||||
</Button>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</form>
|
<div className='spacer' />
|
||||||
</div>
|
<Row>
|
||||||
</Col>
|
<Input
|
||||||
</Row>
|
bsSize='large'
|
||||||
</div>
|
bsStyle='success'
|
||||||
);
|
checked={ isHighlighted.value }
|
||||||
}
|
label={ hightlightCopy }
|
||||||
})
|
onChange={
|
||||||
);
|
({ target: { checked } }) => handleForm({
|
||||||
|
isHighlighted: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={
|
||||||
|
checkboxClass.replace('text-left', '')
|
||||||
|
} />
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
className='text-left'
|
||||||
|
lg={ 6 }
|
||||||
|
lgOffset={ 3 }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
type='submit'>
|
||||||
|
Preview My Ad
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
formOptions,
|
||||||
|
mapStateToProps,
|
||||||
|
bindableActions
|
||||||
|
)(NewJob);
|
||||||
|
@ -1,6 +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 'redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
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';
|
||||||
@ -11,8 +11,9 @@ import JobNotFound from './JobNotFound.jsx';
|
|||||||
import { clearSavedForm, saveJobToDb } from '../redux/actions';
|
import { clearSavedForm, saveJobToDb } from '../redux/actions';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
state => state.jobsApp.form,
|
state => state.jobsApp.previewJob,
|
||||||
(job = {}) => ({ job })
|
state => state.jobsApp.jobs.entities
|
||||||
|
(job, jobsMap) => ({ job: jobsMap[job] || {} })
|
||||||
);
|
);
|
||||||
|
|
||||||
const bindableActions = {
|
const bindableActions = {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { connect, compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { push } from 'react-router-redux';
|
import { push } from 'react-router-redux';
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
@ -2,7 +2,7 @@ import Jobs from './components/Jobs.jsx';
|
|||||||
import NewJob from './components/NewJob.jsx';
|
import NewJob from './components/NewJob.jsx';
|
||||||
import Show from './components/Show.jsx';
|
import Show from './components/Show.jsx';
|
||||||
import Preview from './components/Preview.jsx';
|
import Preview from './components/Preview.jsx';
|
||||||
import GoToPayPal from './components/GoToPayPal.jsx';
|
import JobTotal from './components/JobTotal.jsx';
|
||||||
import NewJobCompleted from './components/NewJobCompleted.jsx';
|
import NewJobCompleted from './components/NewJobCompleted.jsx';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -23,7 +23,7 @@ export default {
|
|||||||
component: Preview
|
component: Preview
|
||||||
}, {
|
}, {
|
||||||
path: 'jobs/new/check-out',
|
path: 'jobs/new/check-out',
|
||||||
component: GoToPayPal
|
component: JobTotal
|
||||||
}, {
|
}, {
|
||||||
path: 'jobs/new/completed',
|
path: 'jobs/new/completed',
|
||||||
component: NewJobCompleted
|
component: NewJobCompleted
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
|
||||||
|
import types from './types';
|
||||||
|
|
||||||
|
export const fetchJobs = createAction(types.fetchJobs);
|
||||||
|
export const fetchJobsCompleted = createAction(
|
||||||
|
types.fetchJobsCompleted,
|
||||||
|
(currentJob, jobs) => ({ currentJob, jobs })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const findJob = createAction(types.findJob);
|
||||||
|
|
||||||
|
export const saveJob = createAction(types.saveJob);
|
||||||
|
export const saveJobCompleted = createAction(types.saveJobCompleted);
|
||||||
|
|
||||||
|
export const saveForm = createAction(types.saveForm);
|
||||||
|
export const clearForm = createAction(types.clearSavedForm);
|
||||||
|
export const loadSavedFormCompleted = createAction(
|
||||||
|
types.loadSavedFormCompleted
|
||||||
|
);
|
||||||
|
39
common/app/routes/Jobs/redux/apply-promo-saga.js
Normal file
39
common/app/routes/Jobs/redux/apply-promo-saga.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
import { testPromo } from './types';
|
||||||
|
import { applyPromo } from './actions';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
|
||||||
|
export default () => ({ dispatch }) => next => {
|
||||||
|
return function applyPromoSaga(action) {
|
||||||
|
if (action.type !== testPromo) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, code = '', type = null } = action.payload;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
id,
|
||||||
|
code: code.replace(/[^\d\w\s]/, '')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
body.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return postJSON$('/api/promos/getButton', body)
|
||||||
|
.retry(3)
|
||||||
|
.map(({ promo }) => {
|
||||||
|
if (!promo || !promo.buttonId) {
|
||||||
|
throw new Error('No promo returned by server');
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyPromo(promo);
|
||||||
|
})
|
||||||
|
.catch(error => Observable.just({
|
||||||
|
type: 'app.handleError',
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
@ -1,7 +1,50 @@
|
|||||||
import { fetchJobs, fetchJobsCompleted } from './types';
|
import { Observable } from 'rx';
|
||||||
|
import { normalize, Schema, arrayOf } from 'normalizr';
|
||||||
|
|
||||||
export default ({ services }) => ({ dispatch, getState }) => next => {
|
import { fetchJobsCompleted } from './actions';
|
||||||
|
import { fetchJobs } from './types';
|
||||||
|
import { handleError } from '../../../redux/types';
|
||||||
|
|
||||||
|
const job = new Schema('job', { idAttribute: 'id' });
|
||||||
|
|
||||||
|
export default ({ services }) => ({ dispatch }) => next => {
|
||||||
return function fetchJobsSaga(action) {
|
return function fetchJobsSaga(action) {
|
||||||
return next(action);
|
if (action.type !== fetchJobs) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { payload: id } = action;
|
||||||
|
const data = { service: 'jobs' };
|
||||||
|
if (id) {
|
||||||
|
data.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return services.readService$(data)
|
||||||
|
.map(jobs => {
|
||||||
|
if (!Array.isArray(jobs)) {
|
||||||
|
jobs = [jobs];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entities, result } = normalize(
|
||||||
|
{ jobs },
|
||||||
|
{ jobs: arrayOf(job) }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return fetchJobsCompleted(
|
||||||
|
result.jobs[0],
|
||||||
|
{
|
||||||
|
entities: entities.jobs,
|
||||||
|
results: result.jobs
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return Observable.just({
|
||||||
|
type: handleError,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.doOnNext(dispatch);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1 +1,8 @@
|
|||||||
export default from './Actions';
|
export actions from './actions';
|
||||||
|
export reducer from './reducer';
|
||||||
|
export types from './types';
|
||||||
|
|
||||||
|
import fetchJobsSaga from './fetch-jobs-saga';
|
||||||
|
import saveJobSaga from './save-job-saga';
|
||||||
|
|
||||||
|
export const sagas = [ fetchJobsSaga, saveJobSaga ];
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
|
import types from './types';
|
||||||
|
|
||||||
|
const replaceMethod = ''.replace;
|
||||||
|
function replace(str) {
|
||||||
|
if (!str) { return ''; }
|
||||||
|
return replaceMethod.call(str, /[^\d\w\s]/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
currentJob: '',
|
||||||
|
newJob: {},
|
||||||
|
jobs: {
|
||||||
|
entities: {},
|
||||||
|
results: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleActions(
|
||||||
|
{
|
||||||
|
[types.findJob]: (state, { payload: id }) => {
|
||||||
|
const currentJob = state.jobs.entities[id];
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentJob: currentJob && currentJob.id ?
|
||||||
|
currentJob.id :
|
||||||
|
state.currentJob
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.saveJobCompleted]: (state, { payload: newJob }) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
newJob
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.fetchJobCompleted]: (state, { payload: { jobs, currentJob } }) => ({
|
||||||
|
...state,
|
||||||
|
currentJob,
|
||||||
|
jobs
|
||||||
|
}),
|
||||||
|
[types.updatePromoCode]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
promoCode: replace(payload)
|
||||||
|
}),
|
||||||
|
[types.applyPromo]: (state, { payload: promo }) => {
|
||||||
|
|
||||||
|
const {
|
||||||
|
fullPrice: price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
code: promoCode,
|
||||||
|
name: promoName
|
||||||
|
} = promo;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
promoCode,
|
||||||
|
promoApplied: true,
|
||||||
|
promoName
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.clearPromo]: state => ({
|
||||||
|
/* eslint-disable no-undefined */
|
||||||
|
...state,
|
||||||
|
price: undefined,
|
||||||
|
buttonId: undefined,
|
||||||
|
discountAmount: undefined,
|
||||||
|
promoCode: undefined,
|
||||||
|
promoApplied: false,
|
||||||
|
promoName: undefined
|
||||||
|
/* eslint-enable no-undefined */
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initialState
|
||||||
|
);
|
||||||
|
26
common/app/routes/Jobs/redux/save-job-saga.js
Normal file
26
common/app/routes/Jobs/redux/save-job-saga.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
import { saveJobCompleted } from './actions';
|
||||||
|
import { saveJob } from './types';
|
||||||
|
|
||||||
|
import { handleError } from '../../../redux/types';
|
||||||
|
|
||||||
|
export default ({ services }) => ({ dispatch }) => next => {
|
||||||
|
return function saveJobSaga(action) {
|
||||||
|
if (action.type !== saveJob) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
const { payload: job } = action;
|
||||||
|
|
||||||
|
return services.createService$({
|
||||||
|
service: 'jobs',
|
||||||
|
params: { job }
|
||||||
|
})
|
||||||
|
.map(job => saveJobCompleted(job))
|
||||||
|
.catch(error => Observable.just({
|
||||||
|
type: handleError,
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
@ -3,16 +3,15 @@ const types = [
|
|||||||
'fetchJobsCompleted',
|
'fetchJobsCompleted',
|
||||||
|
|
||||||
'findJob',
|
'findJob',
|
||||||
|
|
||||||
'saveJob',
|
'saveJob',
|
||||||
'getJob',
|
|
||||||
'getJobs',
|
|
||||||
'openModal',
|
|
||||||
'closeModal',
|
|
||||||
'handleFormUpdate',
|
|
||||||
'saveForm',
|
'saveForm',
|
||||||
'clear'
|
'clearForm',
|
||||||
|
'loadSavedForm',
|
||||||
|
'loadSavedFormCompleted'
|
||||||
];
|
];
|
||||||
|
|
||||||
export default types
|
export default types.reduce((types, type) => {
|
||||||
.reduce((types, type) => ({ ...types, [type]: `jobs.${type}` }), {});
|
types[type] = `jobs.${type}`;
|
||||||
|
return types;
|
||||||
|
}, {});
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { sagas as appSagas } from './redux';
|
import { sagas as appSagas } from './redux';
|
||||||
import { sagas as hikesSagas} from './routes/Hikes/redux';
|
import { sagas as hikesSagas} from './routes/Hikes/redux';
|
||||||
|
import { sagas as jobsSagas } from './routes/Jobs/redux';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...appSagas,
|
...appSagas,
|
||||||
...hikesSagas
|
...hikesSagas,
|
||||||
|
...jobsSagas
|
||||||
];
|
];
|
||||||
|
Reference in New Issue
Block a user