diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js index ee7478011e..82d51d9546 100644 --- a/common/app/create-reducer.js +++ b/common/app/create-reducer.js @@ -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 }); } diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx deleted file mode 100644 index 38f3a15c5b..0000000000 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ /dev/null @@ -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 ( - - -

Promo Discount

- - -

-{ discountAmount }

- -
- ); - }, - - renderHighlightPrice(isHighlighted) { - if (!isHighlighted) { - return null; - } - return ( - - -

Highlighting

- - -

+ 250

- -
- ); - }, - - renderPromo() { - const { - id, - promoApplied, - promoCode, - promoName, - isHighlighted, - jobActions - } = this.props; - if (promoApplied) { - return ( -
-
- - - { promoName } applied - - -
- ); - } - return ( -
-
- - - Have a promo code? - - - - - - - - - - -
- ); - }, - - render() { - const { - id, - isHighlighted, - buttonId, - price, - discountAmount - } = this.props; - - return ( -
- - -
- - -

- One more step -

-
- You're Awesome! just one more step to go. - Clicking on the link below will redirect to paypal. - - -
- - - -

Job Posting

- - -

+ { price }

- -
- { this.renderHighlightPrice(isHighlighted) } - { this.renderDiscount(discountAmount) } - - -

Total

- - -

${ - price - discountAmount + (isHighlighted ? 250 : 0) - }

- -
-
- { this.renderPromo() } -
- - -
- - - - -
- An array of credit cards - - - -
-
- - -
- ); - } - }) -); diff --git a/common/app/routes/Jobs/components/JobNotFound.jsx b/common/app/routes/Jobs/components/JobNotFound.jsx index 343e068f56..e05a886694 100644 --- a/common/app/routes/Jobs/components/JobNotFound.jsx +++ b/common/app/routes/Jobs/components/JobNotFound.jsx @@ -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({
); } -}); +} diff --git a/common/app/routes/Jobs/components/JobTotal.jsx b/common/app/routes/Jobs/components/JobTotal.jsx new file mode 100644 index 0000000000..266d3d9812 --- /dev/null +++ b/common/app/routes/Jobs/components/JobTotal.jsx @@ -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 ( + + +

Promo Discount

+ + +

-{ discountAmount }

+ +
+ ); + } + + renderHighlightPrice(isHighlighted) { + if (!isHighlighted) { + return null; + } + return ( + + +

Highlighting

+ + +

+ 250

+ +
+ ); + } + + renderPromo() { + const { + id, + promoApplied, + promoCode, + promoName, + isHighlighted, + jobActions + } = this.props; + + if (promoApplied) { + return ( +
+
+ + + { promoName } applied + + +
+ ); + } + + return ( +
+
+ + + Have a promo code? + + + + + + + + + + +
+ ); + } + + render() { + const { + id, + isHighlighted, + buttonId, + price, + discountAmount + } = this.props; + + return ( +
+ + +
+ + +

+ One more step +

+
+ You're Awesome! just one more step to go. + Clicking on the link below will redirect to paypal. + + +
+ + + +

Job Posting

+ + +

+ { price }

+ +
+ { this.renderHighlightPrice(isHighlighted) } + { this.renderDiscount(discountAmount) } + + +

Total

+ + +

${ + price - discountAmount + (isHighlighted ? 250 : 0) + }

+ +
+
+ { this.renderPromo() } +
+ + +
+ + + + +
+ An array of credit cards + + + +
+
+ + +
+ ); + } +} + +export default connect(mapStateToProps, bindableActions)(JobTotal); diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 9704d26931..a315227d70 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,132 +1,156 @@ 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: { - children: PropTypes.element, - appActions: PropTypes.object, - jobActions: PropTypes.object, - jobs: PropTypes.array, - showModal: PropTypes.bool - }, +const mapSateToProps = createSelector( + state => state.jobsApp.jobs.entities, + state => state.jobsApp.jobs.results, + state => state.jobsApp, + (jobsMap, jobsById) => { + return jobsById.map(id => jobsMap[id]); + } +); - handleJobClick(id) { - const { appActions, jobActions } = this.props; - if (!id) { - return null; - } - jobActions.findJob(id); - appActions.goTo(`/jobs/${id}`); - }, +const bindableActions = { + findJob, + fetchJobs, + push +}; - renderList(handleJobClick, jobs) { - return ( - - ); - }, +const fetchOptions = { + fetchAction: 'fetchJobs', + isPrimed({ jobs }) { + return !!jobs.results.length; + } +}; - renderChild(child, jobs) { - if (!child) { - return null; - } - return cloneElement( - child, - { jobs } - ); - }, +export class Jobs extends PureComponent { + static displayName = 'Jobs'; - render() { - const { - children, - jobs, - appActions - } = this.props; + static propTypes = { + push: PropTypes.func, + findJob: PropTypes.func, + fetchJobs: PropTypes.func, + children: PropTypes.element, + jobs: PropTypes.array, + showModal: PropTypes.bool + }; - return ( + createJobClickHandler(id) { + const { findJob, push } = this.props; + if (!id) { + return null; + } + + return id => { + findJob(id); + push(`/jobs/${id}`); + }; + } + + renderList(handleJobClick, jobs) { + return ( + + ); + } + + renderChild(child, jobs) { + if (!child) { + return null; + } + return cloneElement( + child, + { jobs } + ); + } + + render() { + const { + children, + jobs + } = this.props; + + return ( + + +

+ Hire a JavaScript engineer who's experienced in HTML5, + Node.js, MongoDB, and Agile Development. +

+
+ + + +
+ + +
-

- Hire a JavaScript engineer who's experienced in HTML5, - Node.js, MongoDB, and Agile Development. -

-
- - - -
- - -
- - - {` - - -
-

- We hired our last developer out of Free Code Camp - and couldn't be happier. Free Code Camp is now - our go-to way to bring on pre-screened candidates - who are enthusiastic about learning quickly and - becoming immediately productive in their new career. -

-
- Michael Gai, CEO at CoNarrative -
-
- -
- + md={ 2 } + xs={ 4 }> + {` + + +
+

+ We hired our last developer out of Free Code Camp + and couldn't be happier. Free Code Camp is now + our go-to way to bring on pre-screened candidates + who are enthusiastic about learning quickly and + becoming immediately productive in their new career. +

+
+ Michael Gai, CEO at CoNarrative +
+
+ +
+ { this.renderChild(children, jobs) || - this.renderList(this.handleJobClick, jobs) } + this.renderList(this.createJobClickHandler(), jobs) } - ); - } - }) -); + ); + } +} + +export default compose( + connect(mapSateToProps, bindableActions), + contain(fetchOptions) +)(Jobs); diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index 4edc4cd5ad..940937436f 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -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,50 +20,50 @@ export default React.createClass({ { locale } ); - }, + } renderJobs(handleClick, jobs = []) { return jobs - .filter(({ isPaid, isApproved, isFilled }) => { - return isPaid && isApproved && !isFilled; - }) - .map(({ - id, - company, - position, - isHighlighted, - locale - }) => { + .filter(({ isPaid, isApproved, isFilled }) => { + return isPaid && isApproved && !isFilled; + }) + .map(({ + id, + company, + position, + isHighlighted, + locale + }) => { - const className = classnames({ - 'jobs-list': true, - 'col-xs-12': true, - 'jobs-list-highlight': isHighlighted - }); - - return ( - handleClick(id) }> -
-

- { company } - {' '} - - - { position } - -

-

- { this.addLocation(locale) } -

-
-
- ); + const className = classnames({ + 'jobs-list': true, + 'col-xs-12': true, + 'jobs-list-highlight': isHighlighted }); - }, + + return ( + handleClick(id) }> +
+

+ { company } + {' '} + + - { position } + +

+

+ { this.addLocation(locale) } +

+
+
+ ); + }); + } render() { const { @@ -76,4 +77,4 @@ export default React.createClass({ ); } -}); +} diff --git a/common/app/routes/Jobs/components/NewJobCompleted.jsx b/common/app/routes/Jobs/components/NewJobCompleted.jsx index 4f11a2371d..8767960e38 100644 --- a/common/app/routes/Jobs/components/NewJobCompleted.jsx +++ b/common/app/routes/Jobs/components/NewJobCompleted.jsx @@ -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({
); } -}); +} diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index 804b34b6d8..a50eca2794 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -1,79 +1,84 @@ 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 }; +import { clearSavedForm, saveJobToDb } from '../redux/actions'; + +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 { push, job } = this.props; + // redirect user in client + if (!job || !job.position || !job.description) { + push('/jobs/new'); } - }, - React.createClass({ - displayName: 'Preview', + } - propTypes: { - appActions: PropTypes.object, - job: PropTypes.object, - jobActions: PropTypes.object - }, + render() { + const { job, goBack, clearSavedForm, saveJobToDb } = this.props; - componentDidMount() { - const { appActions, job } = this.props; - // redirect user in client - if (!job || !job.position || !job.description) { - appActions.goTo('/jobs/new'); - } - }, + if (!job || !job.position || !job.description) { + return ; + } - render() { - const { appActions, job, jobActions } = this.props; + return ( +
+ +
+
+ + +
+
- ); - } - }) -); + ); + } +} + +export default connect(mapStateToProps, bindableActions)(JobPreview); diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 5ccee6e131..84ca630b92 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -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,86 +56,89 @@ function generateMessage( "You've earned it, so feel free to apply."; } -export default contain( - { - store: 'appStore', - fetchAction: 'jobActions.getJob', - map({ - username, - isFrontEndCert, - isBackEndCert, - jobsApp: { currentJob } - }) { - return { - username, - job: currentJob, - isFrontEndCert, - isBackEndCert - }; - }, - getPayload({ params: { id } }) { - return id; - }, - isPrimed({ params: { id } = {}, job = {} }) { - return job.id === id; - }, - // using es6 destructuring - shouldContainerFetch({ job = {} }, { params: { id } } - ) { - return job.id !== id; - } - }, - React.createClass({ - displayName: 'Show', - - propTypes: { - job: PropTypes.object, - isBackEndCert: PropTypes.bool, - isFrontEndCert: PropTypes.bool, - username: PropTypes.string - }, - - mixins: [History], - - componentDidMount() { - const { job } = this.props; - // redirect user in client - if (!isJobValid(job)) { - this.history.pushState(null, '/jobs'); - } - }, - - render() { - const { - isBackEndCert, - isFrontEndCert, - job, - username - } = this.props; - - if (!isJobValid(job)) { - return ; - } - - const isSignedIn = !!username; - - const showApply = shouldShowApply( - job, - { isFrontEndCert, isBackEndCert } - ); - - const message = generateMessage( - job, - { isFrontEndCert, isBackEndCert, isSignedIn } - ); - - return ( - - ); - } +const mapStateToProps = createSelector( + state => state.app, + state => state.jobsApp.currentJob, + ({ username, isFrontEndCert, isBackEndCert }, job = {}) => ({ + username, + isFrontEndCert, + isBackEndCert, + 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 + shouldRefetch({ job }, { params: { id } }) { + return job.id !== id; + } +}; + +export class Show extends PureComponent { + static displayName = 'Show'; + + static propTypes = { + job: PropTypes.object, + isBackEndCert: PropTypes.bool, + isFrontEndCert: PropTypes.bool, + username: PropTypes.string + }; + + componentDidMount() { + const { job, push } = this.props; + // redirect user in client + if (!isJobValid(job)) { + push('/jobs'); + } + } + + render() { + const { + isBackEndCert, + isFrontEndCert, + job, + username + } = this.props; + + if (!isJobValid(job)) { + return ; + } + + const isSignedIn = !!username; + + const showApply = shouldShowApply( + job, + { isFrontEndCert, isBackEndCert } + ); + + const message = generateMessage( + job, + { isFrontEndCert, isBackEndCert, isSignedIn } + ); + + return ( + + ); + } +} + +export default compose( + connect(mapStateToProps, bindableActions), + contain(fetchOptions) +)(Show); diff --git a/common/app/routes/Jobs/redux/actions.js b/common/app/routes/Jobs/redux/actions.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/app/routes/Jobs/redux/fetch-jobs-saga.js b/common/app/routes/Jobs/redux/fetch-jobs-saga.js new file mode 100644 index 0000000000..2712d2e47d --- /dev/null +++ b/common/app/routes/Jobs/redux/fetch-jobs-saga.js @@ -0,0 +1,7 @@ +import { fetchJobs, fetchJobsCompleted } from './types'; + +export default ({ services }) => ({ dispatch, getState }) => next => { + return function fetchJobsSaga(action) { + return next(action); + }; +}; diff --git a/common/app/routes/Jobs/flux/index.js b/common/app/routes/Jobs/redux/index.js similarity index 100% rename from common/app/routes/Jobs/flux/index.js rename to common/app/routes/Jobs/redux/index.js diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/redux/oldActions.js similarity index 100% rename from common/app/routes/Jobs/flux/Actions.js rename to common/app/routes/Jobs/redux/oldActions.js diff --git a/common/app/routes/Jobs/redux/reducer.js b/common/app/routes/Jobs/redux/reducer.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/app/routes/Jobs/redux/types.js b/common/app/routes/Jobs/redux/types.js new file mode 100644 index 0000000000..3e3ccc2b6c --- /dev/null +++ b/common/app/routes/Jobs/redux/types.js @@ -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}` }), {});