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 { reducer as formReducer } from 'redux-form';
import { reducer as app } from './redux';
import { reducer as hikesApp } from './routes/Hikes/redux';
@ -7,6 +8,7 @@ export default function createReducer(sideReducers = {}) {
return combineReducers({
...sideReducers,
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 { Button, Row, Col } from 'react-bootstrap';
export default React.createClass({
displayName: 'NoJobFound',
export default class extends React.Component {
static displayName = 'NoJobFound';
shouldComponentUpdate() {
return false;
}
render() {
return (
@ -28,4 +32,4 @@ export default React.createClass({
</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 { 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 contain from '../../../utils/professor-x';
import ListJobs from './List.jsx';
export default contain(
{
store: 'appStore',
map({ jobsApp: { jobs, showModal }}) {
return { jobs, showModal };
},
fetchAction: 'jobActions.getJobs',
isPrimed({ jobs = [] }) {
return !!jobs.length;
},
actions: [
'appActions',
'jobActions'
]
},
React.createClass({
displayName: 'Jobs',
import {
findJob,
fetchJobs
} from '../redux/actions';
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,
appActions: PropTypes.object,
jobActions: PropTypes.object,
jobs: PropTypes.array,
showModal: PropTypes.bool
},
};
handleJobClick(id) {
const { appActions, jobActions } = this.props;
createJobClickHandler(id) {
const { findJob, push } = this.props;
if (!id) {
return null;
}
jobActions.findJob(id);
appActions.goTo(`/jobs/${id}`);
},
return id => {
findJob(id);
push(`/jobs/${id}`);
};
}
renderList(handleJobClick, jobs) {
return (
@ -45,7 +66,7 @@ export default contain(
handleClick={ handleJobClick }
jobs={ jobs }/>
);
},
}
renderChild(child, jobs) {
if (!child) {
@ -55,13 +76,12 @@ export default contain(
child,
{ jobs }
);
},
}
render() {
const {
children,
jobs,
appActions
jobs
} = this.props;
return (
@ -83,7 +103,7 @@ export default contain(
<Button
className='signup-btn btn-block btn-cta'
onClick={ ()=> {
appActions.goTo('/jobs/new');
push('/jobs/new');
}}>
Post a job: $1,000
</Button>
@ -122,11 +142,15 @@ export default contain(
</Row>
<Row>
{ this.renderChild(children, jobs) ||
this.renderList(this.handleJobClick, jobs) }
this.renderList(this.createJobClickHandler(), jobs) }
</Row>
</Col>
</Row>
);
}
})
);
}
export default compose(
connect(mapSateToProps, bindableActions),
contain(fetchOptions)
)(Jobs);

View File

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

View File

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

View File

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

View File

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