Initial Job move to redux

This commit is contained in:
Berkeley Martinez
2016-02-05 20:48:59 -08:00
parent 5f97394520
commit 371cde1e34
15 changed files with 661 additions and 585 deletions

View File

@ -1,4 +1,5 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
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';
@ -7,6 +8,7 @@ export default function createReducer(sideReducers = {}) {
return combineReducers({ return combineReducers({
...sideReducers, ...sideReducers,
app, app,
hikesApp hikesApp,
form: formReducer
}); });
} }

View File

@ -1,277 +0,0 @@
import React, { PropTypes } from 'react';
import { Button, Input, Col, Row, Well } from 'react-bootstrap';
import { contain } from 'thundercats-react';
// real paypal buttons
// will take your money
const paypalIds = {
regular: 'Q8Z82ZLAX3Q8N',
highlighted: 'VC8QPSKCYMZLN'
};
export default contain(
{
store: 'appStore',
actions: [
'jobActions',
'appActions'
],
map({ jobsApp: {
currentJob: { id, isHighlighted } = {},
buttonId = isHighlighted ?
paypalIds.highlighted :
paypalIds.regular,
price = 1000,
discountAmount = 0,
promoCode = '',
promoApplied = false,
promoName = ''
}}) {
return {
id,
isHighlighted,
buttonId,
price,
discountAmount,
promoName,
promoCode,
promoApplied
};
}
},
React.createClass({
displayName: 'GoToPayPal',
propTypes: {
appActions: PropTypes.object,
id: PropTypes.string,
isHighlighted: PropTypes.bool,
buttonId: PropTypes.string,
price: PropTypes.number,
discountAmount: PropTypes.number,
promoName: PropTypes.string,
promoCode: PropTypes.string,
promoApplied: PropTypes.bool,
jobActions: PropTypes.object
},
componentDidMount() {
const { jobActions } = this.props;
jobActions.clearPromo();
},
goToJobBoard() {
const { appActions } = this.props;
setTimeout(() => appActions.goTo('/jobs'), 0);
},
renderDiscount(discountAmount) {
if (!discountAmount) {
return null;
}
return (
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Promo Discount</h4>
</Col>
<Col
md={ 3 }>
<h4>-{ discountAmount }</h4>
</Col>
</Row>
);
},
renderHighlightPrice(isHighlighted) {
if (!isHighlighted) {
return null;
}
return (
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Highlighting</h4>
</Col>
<Col
md={ 3 }>
<h4>+ 250</h4>
</Col>
</Row>
);
},
renderPromo() {
const {
id,
promoApplied,
promoCode,
promoName,
isHighlighted,
jobActions
} = this.props;
if (promoApplied) {
return (
<div>
<div className='spacer' />
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
{ promoName } applied
</Col>
</Row>
</div>
);
}
return (
<div>
<div className='spacer' />
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
Have a promo code?
</Col>
</Row>
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<Input
onChange={ jobActions.setPromoCode }
type='text'
value={ promoCode } />
</Col>
<Col
md={ 3 }>
<Button
block={ true }
onClick={ () => {
jobActions.applyCode({
id,
code: promoCode,
type: isHighlighted ? 'isHighlighted' : null
});
}}>
Apply Promo Code
</Button>
</Col>
</Row>
</div>
);
},
render() {
const {
id,
isHighlighted,
buttonId,
price,
discountAmount
} = this.props;
return (
<div>
<Row>
<Col
md={ 10 }
mdOffset={ 1 }
sm={ 8 }
smOffset={ 2 }
xs={ 12 }>
<div>
<Row>
<Col
md={ 6 }
mdOffset={ 3 }>
<h2 className='text-center'>
One more step
</h2>
<div className='spacer' />
You're Awesome! just one more step to go.
Clicking on the link below will redirect to paypal.
</Col>
</Row>
<div className='spacer' />
<Well>
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Job Posting</h4>
</Col>
<Col
md={ 6 }>
<h4>+ { price }</h4>
</Col>
</Row>
{ this.renderHighlightPrice(isHighlighted) }
{ this.renderDiscount(discountAmount) }
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Total</h4>
</Col>
<Col
md={ 6 }>
<h4>${
price - discountAmount + (isHighlighted ? 250 : 0)
}</h4>
</Col>
</Row>
</Well>
{ this.renderPromo() }
<div className='spacer' />
<Row>
<Col
md={ 6 }
mdOffset={ 3 }>
<form
action='https://www.paypal.com/cgi-bin/webscr'
method='post'
onClick={ this.goToJobBoard }
target='_blank'>
<input
name='cmd'
type='hidden'
value='_s-xclick' />
<input
name='hosted_button_id'
type='hidden'
value={ buttonId } />
<input
name='custom'
type='hidden'
value={ '' + id } />
<Button
block={ true }
bsSize='large'
className='signup-btn'
type='submit'>
<i className='fa fa-paypal' />
Continue to PayPal
</Button>
<div className='spacer' />
<img
alt='An array of credit cards'
border='0'
src='http://i.imgur.com/Q2SdSZG.png'
style={{
width: '100%'
}} />
</form>
</Col>
</Row>
<div className='spacer' />
</div>
</Col>
</Row>
</div>
);
}
})
);

View File

@ -2,8 +2,12 @@ import React from 'react';
import { LinkContainer } from 'react-router-bootstrap'; import { LinkContainer } from 'react-router-bootstrap';
import { Button, Row, Col } from 'react-bootstrap'; import { Button, Row, Col } from 'react-bootstrap';
export default React.createClass({ export default class extends React.Component {
displayName: 'NoJobFound', static displayName = 'NoJobFound';
shouldComponentUpdate() {
return false;
}
render() { render() {
return ( return (
@ -28,4 +32,4 @@ export default React.createClass({
</div> </div>
); );
} }
}); }

View File

@ -0,0 +1,284 @@
import React, { PropTypes } from 'react';
import { Button, Input, Col, Row, Well } from 'react-bootstrap';
import { connect } from 'react-redux';
import PureComponent from 'react-pure-render/component';
import { createSelector } from 'reselect';
// real paypal buttons
// will take your money
const paypalIds = {
regular: 'Q8Z82ZLAX3Q8N',
highlighted: 'VC8QPSKCYMZLN'
};
const bindableActions = {
};
const mapStateToProps = createSelector(
state => state.jobsApp.currentJob,
state => state.jobsApp,
(
{ id, isHighlighted } = {},
{
buttonId,
price = 1000,
discountAmount = 0,
promoCode = '',
promoApplied = false,
promoName = ''
}
) => {
if (!buttonId) {
buttonId = isHighlighted ?
paypalIds.highlighted :
paypalIds.regular;
}
return {
id,
isHighlighted,
price,
discountAmount,
promoName,
promoCode,
promoApplied
};
}
);
export class JobTotal extends PureComponent {
static displayName = 'JobTotal';
static propTypes = {
id: PropTypes.string,
isHighlighted: PropTypes.bool,
buttonId: PropTypes.string,
price: PropTypes.number,
discountAmount: PropTypes.number,
promoName: PropTypes.string,
promoCode: PropTypes.string,
promoApplied: PropTypes.bool
};
componentDidMount() {
const { jobActions } = this.props;
jobActions.clearPromo();
}
goToJobBoard() {
const { appActions } = this.props;
setTimeout(() => appActions.goTo('/jobs'), 0);
}
renderDiscount(discountAmount) {
if (!discountAmount) {
return null;
}
return (
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Promo Discount</h4>
</Col>
<Col
md={ 3 }>
<h4>-{ discountAmount }</h4>
</Col>
</Row>
);
}
renderHighlightPrice(isHighlighted) {
if (!isHighlighted) {
return null;
}
return (
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Highlighting</h4>
</Col>
<Col
md={ 3 }>
<h4>+ 250</h4>
</Col>
</Row>
);
}
renderPromo() {
const {
id,
promoApplied,
promoCode,
promoName,
isHighlighted,
jobActions
} = this.props;
if (promoApplied) {
return (
<div>
<div className='spacer' />
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
{ promoName } applied
</Col>
</Row>
</div>
);
}
return (
<div>
<div className='spacer' />
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
Have a promo code?
</Col>
</Row>
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<Input
onChange={ jobActions.setPromoCode }
type='text'
value={ promoCode } />
</Col>
<Col
md={ 3 }>
<Button
block={ true }
onClick={ () => {
jobActions.applyCode({
id,
code: promoCode,
type: isHighlighted ? 'isHighlighted' : null
});
}}>
Apply Promo Code
</Button>
</Col>
</Row>
</div>
);
}
render() {
const {
id,
isHighlighted,
buttonId,
price,
discountAmount
} = this.props;
return (
<div>
<Row>
<Col
md={ 10 }
mdOffset={ 1 }
sm={ 8 }
smOffset={ 2 }
xs={ 12 }>
<div>
<Row>
<Col
md={ 6 }
mdOffset={ 3 }>
<h2 className='text-center'>
One more step
</h2>
<div className='spacer' />
You're Awesome! just one more step to go.
Clicking on the link below will redirect to paypal.
</Col>
</Row>
<div className='spacer' />
<Well>
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Job Posting</h4>
</Col>
<Col
md={ 6 }>
<h4>+ { price }</h4>
</Col>
</Row>
{ this.renderHighlightPrice(isHighlighted) }
{ this.renderDiscount(discountAmount) }
<Row>
<Col
md={ 3 }
mdOffset={ 3 }>
<h4>Total</h4>
</Col>
<Col
md={ 6 }>
<h4>${
price - discountAmount + (isHighlighted ? 250 : 0)
}</h4>
</Col>
</Row>
</Well>
{ this.renderPromo() }
<div className='spacer' />
<Row>
<Col
md={ 6 }
mdOffset={ 3 }>
<form
action='https://www.paypal.com/cgi-bin/webscr'
method='post'
onClick={ this.goToJobBoard }
target='_blank'>
<input
name='cmd'
type='hidden'
value='_s-xclick' />
<input
name='hosted_button_id'
type='hidden'
value={ buttonId } />
<input
name='custom'
type='hidden'
value={ '' + id } />
<Button
block={ true }
bsSize='large'
className='signup-btn'
type='submit'>
<i className='fa fa-paypal' />
Continue to PayPal
</Button>
<div className='spacer' />
<img
alt='An array of credit cards'
border='0'
src='http://i.imgur.com/Q2SdSZG.png'
style={{
width: '100%'
}} />
</form>
</Col>
</Row>
<div className='spacer' />
</div>
</Col>
</Row>
</div>
);
}
}
export default connect(mapStateToProps, bindableActions)(JobTotal);

View File

@ -1,43 +1,64 @@
import React, { cloneElement, PropTypes } from 'react'; import React, { cloneElement, PropTypes } from 'react';
import { contain } from 'thundercats-react'; import { connect, compose } from 'redux';
import { createSelector } from 'reselect';
import { push } from 'react-router-redux';
import PureComponent from 'react-pure-render/component';
import { Button, Row, Col } from 'react-bootstrap'; import { Button, Row, Col } from 'react-bootstrap';
import contain from '../../../utils/professor-x';
import ListJobs from './List.jsx'; import ListJobs from './List.jsx';
export default contain( import {
{ findJob,
store: 'appStore', fetchJobs
map({ jobsApp: { jobs, showModal }}) { } from '../redux/actions';
return { jobs, showModal };
},
fetchAction: 'jobActions.getJobs',
isPrimed({ jobs = [] }) {
return !!jobs.length;
},
actions: [
'appActions',
'jobActions'
]
},
React.createClass({
displayName: 'Jobs',
propTypes: { const mapSateToProps = createSelector(
state => state.jobsApp.jobs.entities,
state => state.jobsApp.jobs.results,
state => state.jobsApp,
(jobsMap, jobsById) => {
return jobsById.map(id => jobsMap[id]);
}
);
const bindableActions = {
findJob,
fetchJobs,
push
};
const fetchOptions = {
fetchAction: 'fetchJobs',
isPrimed({ jobs }) {
return !!jobs.results.length;
}
};
export class Jobs extends PureComponent {
static displayName = 'Jobs';
static propTypes = {
push: PropTypes.func,
findJob: PropTypes.func,
fetchJobs: PropTypes.func,
children: PropTypes.element, children: PropTypes.element,
appActions: PropTypes.object,
jobActions: PropTypes.object,
jobs: PropTypes.array, jobs: PropTypes.array,
showModal: PropTypes.bool showModal: PropTypes.bool
}, };
handleJobClick(id) { createJobClickHandler(id) {
const { appActions, jobActions } = this.props; const { findJob, push } = this.props;
if (!id) { if (!id) {
return null; return null;
} }
jobActions.findJob(id);
appActions.goTo(`/jobs/${id}`); return id => {
}, findJob(id);
push(`/jobs/${id}`);
};
}
renderList(handleJobClick, jobs) { renderList(handleJobClick, jobs) {
return ( return (
@ -45,7 +66,7 @@ export default contain(
handleClick={ handleJobClick } handleClick={ handleJobClick }
jobs={ jobs }/> jobs={ jobs }/>
); );
}, }
renderChild(child, jobs) { renderChild(child, jobs) {
if (!child) { if (!child) {
@ -55,13 +76,12 @@ export default contain(
child, child,
{ jobs } { jobs }
); );
}, }
render() { render() {
const { const {
children, children,
jobs, jobs
appActions
} = this.props; } = this.props;
return ( return (
@ -83,7 +103,7 @@ export default contain(
<Button <Button
className='signup-btn btn-block btn-cta' className='signup-btn btn-block btn-cta'
onClick={ ()=> { onClick={ ()=> {
appActions.goTo('/jobs/new'); push('/jobs/new');
}}> }}>
Post a job: $1,000 Post a job: $1,000
</Button> </Button>
@ -122,11 +142,15 @@ export default contain(
</Row> </Row>
<Row> <Row>
{ this.renderChild(children, jobs) || { this.renderChild(children, jobs) ||
this.renderList(this.handleJobClick, jobs) } this.renderList(this.createJobClickHandler(), jobs) }
</Row> </Row>
</Col> </Col>
</Row> </Row>
); );
} }
}) }
);
export default compose(
connect(mapSateToProps, bindableActions),
contain(fetchOptions)
)(Jobs);

View File

@ -1,14 +1,15 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { ListGroup, ListGroupItem } from 'react-bootstrap';
import PureComponent from 'react-pure-render/component';
export default React.createClass({ export default class ListJobs extends PureComponent {
displayName: 'ListJobs', static displayName = 'ListJobs';
propTypes: { static propTypes = {
handleClick: PropTypes.func, handleClick: PropTypes.func,
jobs: PropTypes.array jobs: PropTypes.array
}, };
addLocation(locale) { addLocation(locale) {
if (!locale) { if (!locale) {
@ -19,7 +20,7 @@ export default React.createClass({
{ locale } { locale }
</span> </span>
); );
}, }
renderJobs(handleClick, jobs = []) { renderJobs(handleClick, jobs = []) {
return jobs return jobs
@ -62,7 +63,7 @@ export default React.createClass({
</ListGroupItem> </ListGroupItem>
); );
}); });
}, }
render() { render() {
const { const {
@ -76,4 +77,4 @@ export default React.createClass({
</ListGroup> </ListGroup>
); );
} }
}); }

View File

@ -2,8 +2,8 @@ import React from 'react';
import { LinkContainer } from 'react-router-bootstrap'; import { LinkContainer } from 'react-router-bootstrap';
import { Button, Col, Row } from 'react-bootstrap'; import { Button, Col, Row } from 'react-bootstrap';
export default React.createClass({ export default class extends React.createClass {
displayName: 'NewJobCompleted', static displayName = 'NewJobCompleted';
render() { render() {
return ( return (
@ -36,4 +36,4 @@ export default React.createClass({
</div> </div>
); );
} }
}); }

View File

@ -1,40 +1,44 @@
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 { contain } from 'thundercats-react'; import { connect } from 'redux';
import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component';
import { goBack, push } from 'react-router-redux';
import ShowJob from './ShowJob.jsx'; import ShowJob from './ShowJob.jsx';
import JobNotFound from './JobNotFound.jsx'; import JobNotFound from './JobNotFound.jsx';
export default contain( import { clearSavedForm, saveJobToDb } from '../redux/actions';
{
store: 'appStore',
actions: [
'appActions',
'jobActions'
],
map({ jobsApp: { form: job = {} } }) {
return { job };
}
},
React.createClass({
displayName: 'Preview',
propTypes: { const mapStateToProps = createSelector(
appActions: PropTypes.object, state => state.jobsApp.form,
job: PropTypes.object, (job = {}) => ({ job })
jobActions: PropTypes.object );
},
const bindableActions = {
goBack,
push,
clearSavedForm,
saveJobToDb
};
export class JobPreview extends PureComponent {
static displayName = 'Preview';
static propTypes = {
job: PropTypes.object
};
componentDidMount() { componentDidMount() {
const { appActions, job } = this.props; const { push, job } = this.props;
// redirect user in client // redirect user in client
if (!job || !job.position || !job.description) { if (!job || !job.position || !job.description) {
appActions.goTo('/jobs/new'); push('/jobs/new');
}
} }
},
render() { render() {
const { appActions, job, jobActions } = this.props; const { job, goBack, clearSavedForm, saveJobToDb } = this.props;
if (!job || !job.position || !job.description) { if (!job || !job.position || !job.description) {
return <JobNotFound />; return <JobNotFound />;
@ -55,8 +59,8 @@ export default contain(
block={ true } block={ true }
className='signup-btn' className='signup-btn'
onClick={ () => { onClick={ () => {
jobActions.clearSavedForm(); clearSavedForm();
jobActions.saveJobToDb({ saveJobToDb({
goTo: '/jobs/new/check-out', goTo: '/jobs/new/check-out',
job job
}); });
@ -66,7 +70,7 @@ export default contain(
</Button> </Button>
<Button <Button
block={ true } block={ true }
onClick={ () => appActions.goBack() } > onClick={ goBack } >
Head back and make edits Head back and make edits
</Button> </Button>
</div> </div>
@ -75,5 +79,6 @@ export default contain(
</div> </div>
); );
} }
}) }
);
export default connect(mapStateToProps, bindableActions)(JobPreview);

View File

@ -1,6 +1,11 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { History } from 'react-router'; import { connect, compose } from 'redux';
import { contain } from 'thundercats-react'; import { push } from 'react-router-redux';
import PureComponent from 'react-pure-render/component';
import { createSelector } from 'reselect';
import contain from '../../../utils/professor-x';
import { fetchJob } from '../redux/actions';
import ShowJob from './ShowJob.jsx'; import ShowJob from './ShowJob.jsx';
import JobNotFound from './JobNotFound.jsx'; import JobNotFound from './JobNotFound.jsx';
@ -51,54 +56,53 @@ function generateMessage(
"You've earned it, so feel free to apply."; "You've earned it, so feel free to apply.";
} }
export default contain( const mapStateToProps = createSelector(
{ state => state.app,
store: 'appStore', state => state.jobsApp.currentJob,
fetchAction: 'jobActions.getJob', ({ username, isFrontEndCert, isBackEndCert }, job = {}) => ({
map({
username, username,
isFrontEndCert, isFrontEndCert,
isBackEndCert, isBackEndCert,
jobsApp: { currentJob } job
}) { })
return { );
username,
job: currentJob, const bindableActions = {
isFrontEndCert, push,
isBackEndCert fetchJob
}; };
},
getPayload({ params: { id } }) { const fetchOptions = {
return id; fetchAction: 'fetchJob',
getActionArgs({ params: { id } }) {
return [ id ];
}, },
isPrimed({ params: { id } = {}, job = {} }) { isPrimed({ params: { id } = {}, job = {} }) {
return job.id === id; return job.id === id;
}, },
// using es6 destructuring // using es6 destructuring
shouldContainerFetch({ job = {} }, { params: { id } } shouldRefetch({ job }, { params: { id } }) {
) {
return job.id !== id; return job.id !== id;
} }
}, };
React.createClass({
displayName: 'Show',
propTypes: { export class Show extends PureComponent {
static displayName = 'Show';
static propTypes = {
job: PropTypes.object, job: PropTypes.object,
isBackEndCert: PropTypes.bool, isBackEndCert: PropTypes.bool,
isFrontEndCert: PropTypes.bool, isFrontEndCert: PropTypes.bool,
username: PropTypes.string username: PropTypes.string
}, };
mixins: [History],
componentDidMount() { componentDidMount() {
const { job } = this.props; const { job, push } = this.props;
// redirect user in client // redirect user in client
if (!isJobValid(job)) { if (!isJobValid(job)) {
this.history.pushState(null, '/jobs'); push('/jobs');
}
} }
},
render() { render() {
const { const {
@ -132,5 +136,9 @@ export default contain(
{ ...this.props }/> { ...this.props }/>
); );
} }
}) }
);
export default compose(
connect(mapStateToProps, bindableActions),
contain(fetchOptions)
)(Show);

View File

View File

@ -0,0 +1,7 @@
import { fetchJobs, fetchJobsCompleted } from './types';
export default ({ services }) => ({ dispatch, getState }) => next => {
return function fetchJobsSaga(action) {
return next(action);
};
};

View File

View File

@ -0,0 +1,18 @@
const types = [
'fetchJobs',
'fetchJobsCompleted',
'findJob',
'saveJob',
'getJob',
'getJobs',
'openModal',
'closeModal',
'handleFormUpdate',
'saveForm',
'clear'
];
export default types
.reduce((types, type) => ({ ...types, [type]: `jobs.${type}` }), {});