diff --git a/common/app/Cat.js b/common/app/Cat.js index 5b2040505e..47b31bad6e 100644 --- a/common/app/Cat.js +++ b/common/app/Cat.js @@ -5,7 +5,7 @@ import { Disposable, Observable } from 'rx'; import { post$, postJSON$ } from '../utils/ajax-stream.js'; import { AppActions, AppStore } from './flux'; import { HikesActions } from './routes/Hikes/flux'; -import { JobActions, JobsStore} from './routes/Jobs/flux'; +import { JobActions } from './routes/Jobs/flux'; const ajaxStamp = stamp({ methods: { @@ -22,8 +22,7 @@ export default Cat().init(({ instance: cat, args: [services] }) => { return Observable.create(function(observer) { services.read(resource, params, config, (err, res) => { if (err) { - observer.onError(err); - return observer.onCompleted(); + return observer.onError(err); } observer.onNext(res); @@ -31,8 +30,24 @@ export default Cat().init(({ instance: cat, args: [services] }) => { }); return Disposable.create(function() { + observer.dispose(); + }); + }); + }, + createService$(resource, params, body, config) { + return Observable.create(function(observer) { + services.create(resource, params, body, config, (err, res) => { + if (err) { + return observer.onError(err); + } + + observer.onNext(res); observer.onCompleted(); }); + + return Disposable.create(function() { + observer.dispose(); + }); }); } } @@ -40,9 +55,11 @@ export default Cat().init(({ instance: cat, args: [services] }) => { cat.register(HikesActions.compose(serviceStamp, ajaxStamp), null, services); cat.register(AppActions.compose(serviceStamp), null, services); + cat.register( + JobActions.compose(serviceStamp, ajaxStamp), + null, + cat, + services + ); cat.register(AppStore, null, cat); - - - cat.register(JobActions, null, cat, services); - cat.register(JobsStore.compose(serviceStamp), null, cat); }); diff --git a/common/app/flux/Store.js b/common/app/flux/Store.js index d7dc12e09f..d995544902 100644 --- a/common/app/flux/Store.js +++ b/common/app/flux/Store.js @@ -11,6 +11,9 @@ const initValue = { // lecture state currentHike: {}, showQuestion: false + }, + jobsApp: { + showModal: false } }; @@ -19,25 +22,15 @@ export default Store({ displayName: 'AppStore', value: initValue }, - init({ instance: appStore, args: [cat] }) { + init({ instance: store, args: [cat] }) { + const register = createRegistrar(store); + // app const { updateLocation, getUser, setTitle } = cat.getActions('appActions'); - const register = createRegistrar(appStore); - const { - toggleQuestions, - fetchHikes, - hideInfo, - grabQuestion, - releaseQuestion, - moveQuestion, - answer - } = cat.getActions('hikesActions'); - - // app register( fromMany( setter( @@ -51,6 +44,16 @@ export default Store({ ); // hikes + const { + toggleQuestions, + fetchHikes, + hideInfo, + grabQuestion, + releaseQuestion, + moveQuestion, + answer + } = cat.getActions('hikesActions'); + register( fromMany( toggleQuestions, @@ -63,6 +66,36 @@ export default Store({ ) ); - return appStore; + + // jobs + const { + findJob, + saveJobToDb, + getJob, + getJobs, + openModal, + closeModal, + handleForm, + getSavedForm, + setPromoCode, + applyCode, + clearPromo + } = cat.getActions('JobActions'); + + register( + fromMany( + findJob, + saveJobToDb, + getJob, + getJobs, + openModal, + closeModal, + handleForm, + getSavedForm, + setPromoCode, + applyCode, + clearPromo + ) + ); } }); diff --git a/common/app/routes/Jobs/components/GoToPayPal.jsx b/common/app/routes/Jobs/components/GoToPayPal.jsx index 8b2710b2a8..cf96c9a1c1 100644 --- a/common/app/routes/Jobs/components/GoToPayPal.jsx +++ b/common/app/routes/Jobs/components/GoToPayPal.jsx @@ -11,12 +11,12 @@ const paypalIds = { export default contain( { - store: 'JobsStore', + store: 'appStore', actions: [ 'jobActions', 'appActions' ], - map({ + map({ jobApp: { job: { id, isHighlighted } = {}, buttonId = isHighlighted ? paypalIds.highlighted : @@ -26,7 +26,7 @@ export default contain( promoCode = '', promoApplied = false, promoName - }) { + }}) { return { id, isHighlighted, @@ -57,7 +57,7 @@ export default contain( goToJobBoard() { const { appActions } = this.props; - appActions.updateRoute('/jobs'); + appActions.goTo('/jobs'); }, renderDiscount(discountAmount) { diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 7e23a372f6..b648eeb80f 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -6,7 +6,10 @@ import ListJobs from './List.jsx'; export default contain( { - store: 'jobsStore', + store: 'appStore', + map({ jobsApp: { jobs, showModal }}) { + return { jobs, showModal }; + }, fetchAction: 'jobActions.getJobs', actions: [ 'appActions', @@ -18,25 +21,19 @@ export default contain( propTypes: { children: PropTypes.element, - numOfFollowers: PropTypes.number, appActions: PropTypes.object, jobActions: PropTypes.object, jobs: PropTypes.array, showModal: PropTypes.bool }, - componentDidMount() { - const { jobActions } = this.props; - jobActions.getFollowers(); - }, - handleJobClick(id) { const { appActions, jobActions } = this.props; if (!id) { return null; } jobActions.findJob(id); - appActions.updateRoute(`/jobs/${id}`); + appActions.goTo(`/jobs/${id}`); }, renderList(handleJobClick, jobs) { @@ -84,7 +81,7 @@ export default contain( diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index c482c77094..1eb9178657 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -103,9 +103,9 @@ function makeRequired(validator) { } export default contain({ + store: 'appStore', actions: 'jobActions', - store: 'jobsStore', - map({ form = {} }) { + map({ jobsApp: { form = {} } }) { const { position, locale, diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx index c3cb67c170..532d0d2cdd 100644 --- a/common/app/routes/Jobs/components/Preview.jsx +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -8,12 +8,12 @@ import JobNotFound from './JobNotFound.jsx'; export default contain( { - store: 'JobsStore', + store: 'appStore', actions: [ 'appActions', 'jobActions' ], - map({ form: job = {} }) { + map({ jobApp: { form: job = {} } }) { return { job }; } }, @@ -32,7 +32,7 @@ export default contain( const { appActions, job } = this.props; // redirect user in client if (!job || !job.position || !job.description) { - appActions.updateRoute('/jobs/new'); + appActions.goTo('/jobs/new'); } }, diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 85034e3242..0a2371f91a 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -53,13 +53,14 @@ function generateMessage( export default contain( { - stores: ['appStore', 'jobsStore'], - fetchWaitFor: 'jobsStore', + store: 'appStore', fetchAction: 'jobActions.getJob', - combineLatest( - { username, isFrontEndCert, isFullStackCert }, - { currentJob } - ) { + map({ + username, + isFrontEndCert, + isFullStackCert, + jobsApp: { currentJob } + }) { return { username, job: currentJob, @@ -67,11 +68,11 @@ export default contain( isFullStackCert }; }, - getPayload({ params: { id }, job = {} }) { - return { - id, - isPrimed: job.id === id - }; + getPayload({ params: { id } }) { + return id; + }, + isPrimed({ params: { id } = {}, job = {} }) { + return job.id === id; }, // using es6 destructuring shouldContainerFetch({ job = {} }, { params: { id } } diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 08675048b7..ee50dd419f 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,51 +1,100 @@ import { Actions } from 'thundercats'; import store from 'store'; -import debugFactory from 'debug'; -import { jsonp$ } from '../../../../utils/jsonp$'; -import { postJSON$ } from '../../../../utils/ajax-stream'; +import { nameSpacedTransformer } from '../../../../utils'; -const debug = debugFactory('freecc:jobs:actions'); const assign = Object.assign; +const jobsTranformer = nameSpacedTransformer('jobsApp'); +const noOper = { transform: () => {} }; export default Actions({ - setJobs: null, + refs: { displayName: 'JobActions' }, + shouldBindMethods: true, // findJob assumes that the job is already in the list of jobs findJob(id) { - return oldState => { - const { currentJob = {}, jobs = [] } = oldState; - // currentJob already set - // do nothing - if (currentJob.id === id) { - return null; - } - const foundJob = jobs.reduce((newJob, job) => { - if (job.id === id) { - return job; + return { + transform: jobsTranformer(oldState => { + const { currentJob = {}, jobs = [] } = oldState; + // currentJob already set + // do nothing + if (currentJob.id === id) { + return null; } - return newJob; - }, null); + const foundJob = jobs.reduce((newJob, job) => { + if (job.id === id) { + return job; + } + return newJob; + }, null); - // if no job found this will be null which is a op noop - return foundJob ? - assign({}, oldState, { currentJob: foundJob }) : - null; + // if no job found this will be null which is a op noop + return foundJob ? + assign({}, oldState, { currentJob: foundJob }) : + null; + }) }; }, - setError: null, - getJob: null, - saveJobToDb: null, - getJobs(params) { - return { params }; + saveJobToDb({ goTo, job }) { + return this.createService$('jobs', { job }) + .map(job => ({ + transform(state) { + state.location = { + action: 'PUSH', + pathname: goTo + }; + return { + ...state, + jobs: { + ...state.jobs, + currentJob: job + } + }; + } + })) + .catch(err => ({ + transform(state) { + return { ...state, err }; + } + })); + }, + getJob(id) { + return this.readService$('jobs', { id }) + .map(job => ({ + transform: jobsTranformer(state => { + return { ...state, currentJob: job }; + }) + })) + .catch(err => ({ + transform(state) { + return { ...state, err }; + } + })); + }, + getJobs() { + return this.readService$('jobs') + .map(jobs => ({ + transform: jobsTranformer(state => { + return { ...state, jobs }; + }) + })) + .catch(err => ({ + transform(state) { + return { state, err }; + } + })); }, openModal() { - return { showModal: true }; + return { + transform: jobsTranformer(state => ({ ...state, showModal: true })) + }; }, closeModal() { - return { showModal: false }; + return { + transform: jobsTranformer(state => ({ ...state, showModal: false })) + }; }, handleForm(value) { return { - transform(oldState) { + transform: jobsTranformer(oldState => { const { form } = oldState; const newState = assign({}, oldState); newState.form = assign( @@ -54,142 +103,92 @@ export default Actions({ value ); return newState; - } + }) }; }, saveForm: null, - getSavedForm: null, clearSavedForm: null, - setForm(form) { - return { form }; - }, - getFollowers: null, - setFollowersCount(numOfFollowers) { - return { numOfFollowers }; + getSavedForm() { + const form = store.get('newJob'); + if (form && !Array.isArray(form) && typeof form === 'object') { + return { + transform: jobsTranformer(state => { + return { ...state, form }; + }) + }; + } + return noOper; }, setPromoCode({ target: { value = '' }} = {}) { - return { promoCode: value.replace(/[^\d\w\s]/, '') }; + return { + transform: jobsTranformer(state => ({ + ...state, + promoCode: value.replace(/[^\d\w\s]/, '') + })) + }; + }, + applyCode({ id, code = '', type = null}) { + const body = { + id, + code: code.replace(/[^\d\w\s]/, '') + }; + if (type) { + body.type = type; + } + return this.postJSON$('/api/promos/getButton', body) + .pluck('response') + .map(({ promo }) => { + if (!promo || !promo.buttonId) { + return noOper; + } + const { + fullPrice: price, + buttonId, + discountAmount, + code: promoCode, + name: promoName + } = promo; + + return { + transform: jobsTranformer(state => ({ + ...state, + price, + buttonId, + discountAmount, + promoCode, + promoApplied: true, + promoName + })) + }; + }) + .catch(err => ({ + transform(state) { + return { ...state, err }; + } + })); }, - applyCode: null, clearPromo(foo, undef) { return { - price: undef, - buttonId: undef, - discountAmount: undef, - promoCode: undef, - promoApplied: false, - promoName: undef + transform: jobsTranformer(state => ({ + ...state, + price: undef, + buttonId: undef, + discountAmount: undef, + promoCode: undef, + promoApplied: false, + promoName: undef + })) }; }, - applyPromo({ - fullPrice: price, - buttonId, - discountAmount, - code: promoCode, - name: promoName - } = {}) { - return { - price, - buttonId, - discountAmount, - promoCode, - promoApplied: true, - promoName - }; - } -}) - .refs({ displayName: 'JobActions' }) - .init(({ instance: jobActions, args: [cat, services] }) => { - jobActions.getJobs.subscribe(() => { - services.read('jobs', null, null, (err, jobs) => { - if (err) { - debug('job services experienced an issue', err); - return jobActions.setError({ err }); - } - jobActions.setJobs({ jobs }); - }); - }); - - jobActions.getJob.subscribe(({ id, isPrimed }) => { - // job is already set, do nothing. - if (isPrimed) { - debug('job is primed'); - return; - } - services.read('jobs', { id }, null, (err, job) => { - if (err) { - debug('job services experienced an issue', err); - return jobActions.setError({ err }); - } - if (job) { - jobActions.setJobs({ currentJob: job }); - } - jobActions.setJobs({}); - }); - }); - + init({ instance: jobActions }) { jobActions.saveForm.subscribe((form) => { store.set('newJob', form); }); - jobActions.getSavedForm.subscribe(() => { - const job = store.get('newJob'); - if (job && !Array.isArray(job) && typeof job === 'object') { - jobActions.setForm(job); - } - }); - jobActions.clearSavedForm.subscribe(() => { store.remove('newJob'); }); - jobActions.saveJobToDb.subscribe(({ goTo, job }) => { - const appActions = cat.getActions('appActions'); - services.create('jobs', { job }, null, (err, job) => { - if (err) { - debug('job services experienced an issue', err); - return jobActions.setError(err); - } - jobActions.setJobs({ job }); - appActions.updateRoute(goTo); - }); - }); - - jobActions.getFollowers.subscribe(() => { - const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' + - 'info.json?lang=en&screen_names=CamperJobs' + - '&callback=JSONPCallback'; - - jsonp$(url) - .map(({ response }) => { - return response[0]['followers_count']; - }) - .subscribe( - count => jobActions.setFollowersCount(count), - err => jobActions.setError(err) - ); - }); - - jobActions.applyCode.subscribe(({ id, code = '', type = null}) => { - const body = { - id, - code: code.replace(/[^\d\w\s]/, '') - }; - if (type) { - body.type = type; - } - postJSON$('/api/promos/getButton', body) - .pluck('response') - .subscribe( - ({ promo }) => { - if (promo && promo.buttonId) { - jobActions.applyPromo(promo); - } - jobActions.setError(new Error('no promo found')); - }, - jobActions.setError - ); - }); - return jobActions; - }); + } +}); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js deleted file mode 100644 index b7aa5cda3d..0000000000 --- a/common/app/routes/Jobs/flux/Store.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Store } from 'thundercats'; - -const { - createRegistrar, - setter, - transformer -} = Store; - -export default Store({ - refs: { - displayName: 'JobsStore', - value: { showModal: false } - }, - init({ instance: jobsStore, args: [cat] }) { - const { - setJobs, - findJob, - setError, - openModal, - closeModal, - handleForm, - setForm, - setFollowersCount, - setPromoCode, - applyPromo, - clearPromo - } = cat.getActions('JobActions'); - const register = createRegistrar(jobsStore); - register(setter(setJobs)); - register(setter(setError)); - register(setter(openModal)); - register(setter(closeModal)); - register(setter(setForm)); - register(setter(setPromoCode)); - register(setter(applyPromo)); - register(setter(clearPromo)); - register(setter(setFollowersCount)); - - register(transformer(findJob)); - register(handleForm); - } -}); diff --git a/common/app/routes/Jobs/flux/index.js b/common/app/routes/Jobs/flux/index.js index de123cba0d..61944c2666 100644 --- a/common/app/routes/Jobs/flux/index.js +++ b/common/app/routes/Jobs/flux/index.js @@ -1,2 +1 @@ export { default as JobActions } from './Actions'; -export { default as JobsStore } from './Store'; diff --git a/common/utils/index.js b/common/utils/index.js new file mode 100644 index 0000000000..0456c3bbbe --- /dev/null +++ b/common/utils/index.js @@ -0,0 +1,13 @@ +export function nameSpacedTransformer(ns, transformer) { + if (!transformer) { + return nameSpacedTransformer.bind(null, ns); + } + return (state) => { + const newState = transformer(state[ns]); + // nothing has changed + if (newState === state[ns]) { + return state; + } + return { ...state, [ns]: newState }; + }; +}