Wrap up flux actions

This commit is contained in:
Berkeley Martinez
2016-02-25 18:30:10 -08:00
parent 371cde1e34
commit 056d749ddd
15 changed files with 655 additions and 394 deletions

View 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);
};
};

View File

@ -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;
}, {});

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View 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);
};
};

View File

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

View File

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

View File

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

View 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);
};
};

View File

@ -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;
}, {});

View File

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