Merge pull request #7214 from FreeCodeCamp/feature/redux
Move video/jobs to redux
This commit is contained in:
19
.eslintrc
19
.eslintrc
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"ecmaFeatures": {
|
"parserOption": {
|
||||||
"jsx": true
|
"ecmaVersion": 6,
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
@ -12,6 +15,7 @@
|
|||||||
"react"
|
"react"
|
||||||
],
|
],
|
||||||
"globals": {
|
"globals": {
|
||||||
|
"Promise": true,
|
||||||
"window": true,
|
"window": true,
|
||||||
"$": true,
|
"$": true,
|
||||||
"ga": true,
|
"ga": true,
|
||||||
@ -58,7 +62,6 @@
|
|||||||
"no-caller": 2,
|
"no-caller": 2,
|
||||||
"no-div-regex": 2,
|
"no-div-regex": 2,
|
||||||
"no-else-return": 0,
|
"no-else-return": 0,
|
||||||
"no-empty-label": 2,
|
|
||||||
"no-eq-null": 1,
|
"no-eq-null": 1,
|
||||||
"no-eval": 2,
|
"no-eval": 2,
|
||||||
"no-extend-native": 2,
|
"no-extend-native": 2,
|
||||||
@ -182,10 +185,7 @@
|
|||||||
"always"
|
"always"
|
||||||
],
|
],
|
||||||
"sort-vars": 0,
|
"sort-vars": 0,
|
||||||
"space-after-keywords": [
|
"keyword-spacing": [ 2 ],
|
||||||
2,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"space-before-function-paren": [
|
"space-before-function-paren": [
|
||||||
2,
|
2,
|
||||||
"never"
|
"never"
|
||||||
@ -197,7 +197,6 @@
|
|||||||
"space-in-brackets": 0,
|
"space-in-brackets": 0,
|
||||||
"space-in-parens": 0,
|
"space-in-parens": 0,
|
||||||
"space-infix-ops": 2,
|
"space-infix-ops": 2,
|
||||||
"space-return-throw-case": 2,
|
|
||||||
"space-unary-ops": [
|
"space-unary-ops": [
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
@ -214,7 +213,7 @@
|
|||||||
|
|
||||||
"max-depth": 0,
|
"max-depth": 0,
|
||||||
"max-len": [
|
"max-len": [
|
||||||
1,
|
2,
|
||||||
80,
|
80,
|
||||||
2
|
2
|
||||||
],
|
],
|
||||||
@ -232,7 +231,7 @@
|
|||||||
"react/jsx-uses-vars": 1,
|
"react/jsx-uses-vars": 1,
|
||||||
"react/no-did-mount-set-state": 2,
|
"react/no-did-mount-set-state": 2,
|
||||||
"react/no-did-update-set-state": 2,
|
"react/no-did-update-set-state": 2,
|
||||||
"react/no-multi-comp": 2,
|
"react/no-multi-comp": [2, { "ignoreStateless": true } ],
|
||||||
"react/prop-types": 2,
|
"react/prop-types": 2,
|
||||||
"react/react-in-jsx-scope": 1,
|
"react/react-in-jsx-scope": 1,
|
||||||
"react/self-closing-comp": 1,
|
"react/self-closing-comp": 1,
|
||||||
|
@ -34,6 +34,7 @@ window.common = (function(global) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
isAlive: function(key) {
|
isAlive: function(key) {
|
||||||
|
@ -51,10 +51,10 @@ $(document).ready(function() {
|
|||||||
<h1>${err}</h1>
|
<h1>${err}</h1>
|
||||||
`).subscribe(() => {});
|
`).subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
err => console.error(err)
|
err => console.error(err)
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
common.resetBtn$
|
common.resetBtn$
|
||||||
@ -74,6 +74,7 @@ $(document).ready(function() {
|
|||||||
common.codeStorage.updateStorage(challengeName, originalCode);
|
common.codeStorage.updateStorage(challengeName, originalCode);
|
||||||
common.codeUri.querify(originalCode);
|
common.codeUri.querify(originalCode);
|
||||||
common.updateOutputDisplay(output);
|
common.updateOutputDisplay(output);
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -112,6 +113,7 @@ $(document).ready(function() {
|
|||||||
if (solved) {
|
if (solved) {
|
||||||
common.showCompletion();
|
common.showCompletion();
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
({ err }) => {
|
({ err }) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -138,6 +140,7 @@ $(document).ready(function() {
|
|||||||
return common.updateOutputDisplay('' + err);
|
return common.updateOutputDisplay('' + err);
|
||||||
}
|
}
|
||||||
common.displayTestResults(tests);
|
common.displayTestResults(tests);
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
({ err }) => {
|
({ err }) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -149,7 +152,7 @@ $(document).ready(function() {
|
|||||||
challengeType === challengeTypes.BONFIRE ||
|
challengeType === challengeTypes.BONFIRE ||
|
||||||
challengeType === challengeTypes.JS
|
challengeType === challengeTypes.JS
|
||||||
) {
|
) {
|
||||||
Observable.just({})
|
return Observable.just({})
|
||||||
.delay(500)
|
.delay(500)
|
||||||
.flatMap(() => common.executeChallenge$())
|
.flatMap(() => common.executeChallenge$())
|
||||||
.catch(err => Observable.just({ err }))
|
.catch(err => Observable.just({ err }))
|
||||||
@ -161,6 +164,7 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
common.codeStorage.updateStorage(challengeName, originalCode);
|
common.codeStorage.updateStorage(challengeName, originalCode);
|
||||||
common.displayTestResults(tests);
|
common.displayTestResults(tests);
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -168,4 +172,5 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
@ -110,7 +110,7 @@ window.common = (function({ common = { init: [] }}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
execInProgress = true;
|
execInProgress = true;
|
||||||
setTimeout(function() {
|
return setTimeout(function() {
|
||||||
if (
|
if (
|
||||||
$($('.scroll-locker').children()[0]).height() - 800 > e.detail
|
$($('.scroll-locker').children()[0]).height() - 800 > e.detail
|
||||||
) {
|
) {
|
||||||
|
@ -92,18 +92,16 @@ window.common = (function({ $, common = { init: [] }}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleActionClick(e) {
|
function handleActionClick(e) {
|
||||||
var props = common.challengeSeed[0] ||
|
var props = common.challengeSeed[0] || { stepIndex: [] };
|
||||||
{ stepIndex: [] };
|
|
||||||
|
|
||||||
var $el = $(this);
|
var $el = $(this);
|
||||||
var index = +$el.attr('id');
|
var index = +$el.attr('id');
|
||||||
var propIndex = props.stepIndex.indexOf(index);
|
var propIndex = props.stepIndex.indexOf(index);
|
||||||
|
|
||||||
if (propIndex === -1) {
|
if (propIndex === -1) {
|
||||||
return $el
|
return $el.parent()
|
||||||
.parent()
|
.find('.disabled')
|
||||||
.find('.disabled')
|
.removeClass('disabled');
|
||||||
.removeClass('disabled');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// an API action
|
// an API action
|
||||||
@ -112,30 +110,26 @@ window.common = (function({ $, common = { init: [] }}) {
|
|||||||
var prop = props.properties[propIndex];
|
var prop = props.properties[propIndex];
|
||||||
var api = props.apis[propIndex];
|
var api = props.apis[propIndex];
|
||||||
if (common[prop]) {
|
if (common[prop]) {
|
||||||
return $el
|
return $el.parent()
|
||||||
.parent()
|
|
||||||
.find('.disabled')
|
|
||||||
.removeClass('disabled');
|
|
||||||
}
|
|
||||||
$
|
|
||||||
.post(api)
|
|
||||||
.done(function(data) {
|
|
||||||
// assume a boolean indicates passing
|
|
||||||
if (typeof data === 'boolean') {
|
|
||||||
return $el
|
|
||||||
.parent()
|
|
||||||
.find('.disabled')
|
.find('.disabled')
|
||||||
.removeClass('disabled');
|
.removeClass('disabled');
|
||||||
}
|
}
|
||||||
// assume api returns string when fails
|
return $.post(api)
|
||||||
$el
|
.done(function(data) {
|
||||||
.parent()
|
// assume a boolean indicates passing
|
||||||
.find('.disabled')
|
if (typeof data === 'boolean') {
|
||||||
.replaceWith('<p>' + data + '</p>');
|
return $el.parent()
|
||||||
})
|
.find('.disabled')
|
||||||
.fail(function() {
|
.removeClass('disabled');
|
||||||
console.log('failed');
|
}
|
||||||
});
|
// assume api returns string when fails
|
||||||
|
return $el.parent()
|
||||||
|
.find('.disabled')
|
||||||
|
.replaceWith('<p>' + data + '</p>');
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
console.log('failed');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFinishClick(e) {
|
function handleFinishClick(e) {
|
||||||
@ -199,6 +193,7 @@ window.common = (function({ $, common = { init: [] }}) {
|
|||||||
$(nextBtnClass).click(handleNextStepClick);
|
$(nextBtnClass).click(handleNextStepClick);
|
||||||
$(actionBtnClass).click(handleActionClick);
|
$(actionBtnClass).click(handleActionClick);
|
||||||
$(finishBtnClass).click(handleFinishClick);
|
$(finishBtnClass).click(handleFinishClick);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return common;
|
return common;
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export default function toastSaga(err$, toast) {
|
|
||||||
err$
|
|
||||||
.doOnNext(() => toast({
|
|
||||||
type: 'error',
|
|
||||||
title: 'Oops, something went wrong',
|
|
||||||
message: `Something went wrong, please try again later`
|
|
||||||
}))
|
|
||||||
.subscribe(err => console.error(err));
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
import { Disposable, Observable } from 'rx';
|
|
||||||
|
|
||||||
export function location$(history) {
|
|
||||||
return Observable.create(function(observer) {
|
|
||||||
const dispose = history.listen(function(location) {
|
|
||||||
observer.onNext(location);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Disposable.create(() => {
|
|
||||||
dispose();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyLocation = {
|
|
||||||
pathname: '',
|
|
||||||
search: '',
|
|
||||||
hash: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
let prevKey;
|
|
||||||
let isSyncing = false;
|
|
||||||
export default function historySaga(
|
|
||||||
history,
|
|
||||||
updateLocation,
|
|
||||||
goTo,
|
|
||||||
goBack,
|
|
||||||
routerState$
|
|
||||||
) {
|
|
||||||
routerState$.subscribe(
|
|
||||||
location => {
|
|
||||||
|
|
||||||
if (!location) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// store location has changed, update history
|
|
||||||
if (!location.key || location.key !== prevKey) {
|
|
||||||
isSyncing = true;
|
|
||||||
history.transitionTo({ ...emptyLocation, ...location });
|
|
||||||
isSyncing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
location$(history)
|
|
||||||
.doOnNext(location => {
|
|
||||||
prevKey = location.key;
|
|
||||||
|
|
||||||
if (isSyncing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateLocation(location);
|
|
||||||
})
|
|
||||||
.subscribe(() => {});
|
|
||||||
|
|
||||||
goTo
|
|
||||||
.doOnNext((route = '/') => {
|
|
||||||
history.push(route);
|
|
||||||
})
|
|
||||||
.subscribe(() => {});
|
|
||||||
|
|
||||||
goBack
|
|
||||||
.doOnNext(() => {
|
|
||||||
history.goBack();
|
|
||||||
})
|
|
||||||
.subscribe(() => {});
|
|
||||||
}
|
|
126
client/index.js
126
client/index.js
@ -1,99 +1,71 @@
|
|||||||
import unused from './es6-shims'; // eslint-disable-line
|
import './es6-shims';
|
||||||
import Rx from 'rx';
|
import Rx from 'rx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Fetchr from 'fetchr';
|
import debug from 'debug';
|
||||||
import debugFactory from 'debug';
|
|
||||||
import { Router } from 'react-router';
|
import { Router } from 'react-router';
|
||||||
import { createLocation, createHistory } from 'history';
|
import { routeReducer as routing, syncHistory } from 'react-router-redux';
|
||||||
import { hydrate } from 'thundercats';
|
import { createHistory } from 'history';
|
||||||
import { render$ } from 'thundercats-react';
|
|
||||||
|
|
||||||
import app$ from '../common/app';
|
import app$ from '../common/app';
|
||||||
import historySaga from './history-saga';
|
import provideStore from '../common/app/provide-store';
|
||||||
import errSaga from './err-saga';
|
|
||||||
|
|
||||||
const debug = debugFactory('fcc:client');
|
// client specific sagas
|
||||||
|
import sagas from './sagas';
|
||||||
|
|
||||||
|
// render to observable
|
||||||
|
import render from '../common/app/utils/render';
|
||||||
|
|
||||||
|
const log = debug('fcc:client');
|
||||||
const DOMContianer = document.getElementById('fcc');
|
const DOMContianer = document.getElementById('fcc');
|
||||||
const catState = window.__fcc__.data || {};
|
const initialState = window.__fcc__.data;
|
||||||
const services = new Fetchr({
|
|
||||||
xhrPath: '/services'
|
const serviceOptions = { xhrPath: '/services' };
|
||||||
});
|
|
||||||
|
|
||||||
Rx.config.longStackSupport = !!debug.enabled;
|
Rx.config.longStackSupport = !!debug.enabled;
|
||||||
const history = createHistory();
|
const history = createHistory();
|
||||||
const appLocation = createLocation(
|
const appLocation = history.createLocation(
|
||||||
location.pathname + location.search
|
location.pathname + location.search
|
||||||
);
|
);
|
||||||
|
const routingMiddleware = syncHistory(history);
|
||||||
|
|
||||||
|
const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f;
|
||||||
|
const shouldRouterListenForReplays = !!window.devToolsExtension;
|
||||||
|
|
||||||
|
const clientSagaOptions = { doc: document };
|
||||||
|
|
||||||
// returns an observable
|
// returns an observable
|
||||||
app$({ history, location: appLocation })
|
app$({
|
||||||
.flatMap(
|
location: appLocation,
|
||||||
({ AppCat }) => {
|
history,
|
||||||
// instantiate the cat with service
|
serviceOptions,
|
||||||
const appCat = AppCat(null, services, history);
|
initialState,
|
||||||
// hydrate the stores
|
middlewares: [
|
||||||
return hydrate(appCat, catState).map(() => appCat);
|
routingMiddleware,
|
||||||
},
|
...sagas.map(saga => saga(clientSagaOptions))
|
||||||
// not using nextLocation at the moment but will be used for
|
],
|
||||||
// redirects in the future
|
reducers: { routing },
|
||||||
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
enhancers: [ devTools ]
|
||||||
)
|
})
|
||||||
.doOnNext(({ appCat }) => {
|
.flatMap(({ props, store }) => {
|
||||||
const appStore$ = appCat.getStore('appStore');
|
|
||||||
|
|
||||||
const {
|
// because of weirdness in react-routers match function
|
||||||
toast,
|
// we replace the wrapped returned in props with the first one
|
||||||
updateLocation,
|
// we passed in. This might be fixed in react-router 2.0
|
||||||
goTo,
|
|
||||||
goBack
|
|
||||||
} = appCat.getActions('appActions');
|
|
||||||
|
|
||||||
|
|
||||||
const routerState$ = appStore$
|
|
||||||
.map(({ location }) => location)
|
|
||||||
.filter(location => !!location);
|
|
||||||
|
|
||||||
// set page title
|
|
||||||
appStore$
|
|
||||||
.pluck('title')
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.doOnNext(title => document.title = title)
|
|
||||||
.subscribe(() => {});
|
|
||||||
|
|
||||||
historySaga(
|
|
||||||
history,
|
|
||||||
updateLocation,
|
|
||||||
goTo,
|
|
||||||
goBack,
|
|
||||||
routerState$
|
|
||||||
);
|
|
||||||
|
|
||||||
const err$ = appStore$
|
|
||||||
.pluck('err')
|
|
||||||
.filter(err => !!err)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
errSaga(err$, toast);
|
|
||||||
})
|
|
||||||
// allow store subscribe to subscribe to actions
|
|
||||||
.delay(10)
|
|
||||||
.flatMap(({ props, appCat }) => {
|
|
||||||
props.history = history;
|
props.history = history;
|
||||||
|
|
||||||
return render$(
|
if (shouldRouterListenForReplays && store) {
|
||||||
appCat,
|
log('routing middleware listening for replays');
|
||||||
React.createElement(Router, props),
|
routingMiddleware.listenForReplays(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
log('rendering');
|
||||||
|
return render(
|
||||||
|
provideStore(React.createElement(Router, props), store),
|
||||||
DOMContianer
|
DOMContianer
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => debug('react rendered'),
|
||||||
debug('react rendered');
|
err => { throw err; },
|
||||||
},
|
() => debug('react closed subscription')
|
||||||
err => {
|
|
||||||
throw err;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
debug('react closed subscription');
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
@ -93,6 +93,7 @@ main = (function(main, global) {
|
|||||||
'<span>Free Code Camp\'s Main Chat</span>' +
|
'<span>Free Code Camp\'s Main Chat</span>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -233,7 +234,7 @@ $(document).ready(function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$('#story-submit').unbind('click');
|
$('#story-submit').unbind('click');
|
||||||
$.post('/stories/', data)
|
return $.post('/stories/', data)
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
$('#story-submit').bind('click', storySubmitButtonHandler);
|
$('#story-submit').bind('click', storySubmitButtonHandler);
|
||||||
})
|
})
|
||||||
@ -243,6 +244,7 @@ $(document).ready(function() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
window.location = '/stories/' + storyLink;
|
window.location = '/stories/' + storyLink;
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
0
client/sagas/README.md
Normal file
0
client/sagas/README.md
Normal file
20
client/sagas/err-saga.js
Normal file
20
client/sagas/err-saga.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// () =>
|
||||||
|
// (store: Store) =>
|
||||||
|
// (next: (action: Action) => Object) =>
|
||||||
|
// errSaga(action: Action) => Object|Void
|
||||||
|
export default () => ({ dispatch }) => next => {
|
||||||
|
return function errorSaga(action) {
|
||||||
|
const result = next(action);
|
||||||
|
if (!action.error) { return result; }
|
||||||
|
|
||||||
|
console.error(action.error);
|
||||||
|
return dispatch({
|
||||||
|
type: 'app.makeToast',
|
||||||
|
payload: {
|
||||||
|
type: 'error',
|
||||||
|
title: 'Oops, something went wrong',
|
||||||
|
message: 'Something went wrong, please try again later'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
24
client/sagas/hard-go-to-saga.js
Normal file
24
client/sagas/hard-go-to-saga.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { hardGoTo } from '../../common/app/redux/types';
|
||||||
|
|
||||||
|
const loc = typeof window !== 'undefined' ?
|
||||||
|
window.location :
|
||||||
|
{};
|
||||||
|
|
||||||
|
export default () => ({ dispatch }) => next => {
|
||||||
|
return function hardGoToSaga(action) {
|
||||||
|
const result = next(action);
|
||||||
|
if (action.type !== hardGoTo) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loc.pathname) {
|
||||||
|
dispatch({
|
||||||
|
type: 'app.error',
|
||||||
|
error: new Error('no location object found')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loc.pathname = action.payload || '/map';
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
};
|
6
client/sagas/index.js
Normal file
6
client/sagas/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import errSaga from './err-saga';
|
||||||
|
import titleSaga from './title-saga';
|
||||||
|
import localStorageSaga from './local-storage-saga';
|
||||||
|
import hardGoToSaga from './hard-go-to-saga';
|
||||||
|
|
||||||
|
export default [ errSaga, titleSaga, localStorageSaga, hardGoToSaga ];
|
69
client/sagas/local-storage-saga.js
Normal file
69
client/sagas/local-storage-saga.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
saveForm,
|
||||||
|
clearForm,
|
||||||
|
loadSavedForm
|
||||||
|
} from '../../common/app/routes/Jobs/redux/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
saveCompleted,
|
||||||
|
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));
|
||||||
|
next(action);
|
||||||
|
return dispatch(saveCompleted(form));
|
||||||
|
} catch (error) {
|
||||||
|
return dispatch({
|
||||||
|
type: 'app.handleError',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (error) {
|
||||||
|
return dispatch({
|
||||||
|
type: 'app.handleError',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
};
|
17
client/sagas/title-saga.js
Normal file
17
client/sagas/title-saga.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// (doc: Object) =>
|
||||||
|
// () =>
|
||||||
|
// (next: (action: Action) => Object) =>
|
||||||
|
// titleSage(action: Action) => Object|Void
|
||||||
|
export default ({ doc }) => ({ getState }) => next => {
|
||||||
|
return function titleSage(action) {
|
||||||
|
// get next state
|
||||||
|
const result = next(action);
|
||||||
|
if (action.type !== 'app.updateTitle') {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const state = getState();
|
||||||
|
const newTitle = state.app.title;
|
||||||
|
doc.title = newTitle;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
@ -1,81 +1,89 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Row } from 'react-bootstrap';
|
import { Row } from 'react-bootstrap';
|
||||||
import { ToastMessage, ToastContainer } from 'react-toastr';
|
import { ToastMessage, ToastContainer } from 'react-toastr';
|
||||||
import { contain } from 'thundercats-react';
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { fetchUser } from './redux/actions';
|
||||||
|
import contain from './utils/professor-x';
|
||||||
|
|
||||||
import Nav from './components/Nav';
|
import Nav from './components/Nav';
|
||||||
|
|
||||||
const toastMessageFactory = React.createFactory(ToastMessage.animation);
|
const toastMessageFactory = React.createFactory(ToastMessage.animation);
|
||||||
|
|
||||||
export default contain(
|
const mapStateToProps = createSelector(
|
||||||
{
|
state => state.app,
|
||||||
actions: ['appActions'],
|
({
|
||||||
store: 'appStore',
|
username,
|
||||||
fetchAction: 'appActions.getUser',
|
points,
|
||||||
isPrimed({ username }) {
|
picture,
|
||||||
return !!username;
|
toast
|
||||||
},
|
}) => ({
|
||||||
map({
|
username,
|
||||||
username,
|
points,
|
||||||
points,
|
picture,
|
||||||
picture,
|
toast
|
||||||
toast
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
username,
|
|
||||||
points,
|
|
||||||
picture,
|
|
||||||
toast
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getPayload(props) {
|
|
||||||
return {
|
|
||||||
isPrimed: !!props.username
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
React.createClass({
|
|
||||||
displayName: 'FreeCodeCamp',
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
appActions: PropTypes.object,
|
|
||||||
children: PropTypes.node,
|
|
||||||
username: PropTypes.string,
|
|
||||||
points: PropTypes.number,
|
|
||||||
picture: PropTypes.string,
|
|
||||||
toast: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps({ toast: nextToast = {} }) {
|
|
||||||
const { toast = {} } = this.props;
|
|
||||||
if (toast.id !== nextToast.id) {
|
|
||||||
this.refs.toaster[nextToast.type || 'success'](
|
|
||||||
nextToast.message,
|
|
||||||
nextToast.title,
|
|
||||||
{
|
|
||||||
closeButton: true,
|
|
||||||
timeOut: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { username, points, picture } = this.props;
|
|
||||||
const navProps = { username, points, picture };
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Nav
|
|
||||||
{ ...navProps }/>
|
|
||||||
<Row>
|
|
||||||
{ this.props.children }
|
|
||||||
</Row>
|
|
||||||
<ToastContainer
|
|
||||||
className='toast-bottom-right'
|
|
||||||
ref='toaster'
|
|
||||||
toastMessageFactory={ toastMessageFactory } />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fetchContainerOptions = {
|
||||||
|
fetchAction: 'fetchUser',
|
||||||
|
isPrimed({ username }) {
|
||||||
|
return !!username;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// export plain class for testing
|
||||||
|
export class FreeCodeCamp extends React.Component {
|
||||||
|
static displayName = 'FreeCodeCamp';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
username: PropTypes.string,
|
||||||
|
points: PropTypes.number,
|
||||||
|
picture: PropTypes.string,
|
||||||
|
toast: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillReceiveProps({ toast: nextToast = {} }) {
|
||||||
|
const { toast = {} } = this.props;
|
||||||
|
if (toast.id !== nextToast.id) {
|
||||||
|
this.refs.toaster[nextToast.type || 'success'](
|
||||||
|
nextToast.message,
|
||||||
|
nextToast.title,
|
||||||
|
{
|
||||||
|
closeButton: true,
|
||||||
|
timeOut: 10000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { username, points, picture } = this.props;
|
||||||
|
const navProps = { username, points, picture };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Nav { ...navProps }/>
|
||||||
|
<Row>
|
||||||
|
{ this.props.children }
|
||||||
|
</Row>
|
||||||
|
<ToastContainer
|
||||||
|
className='toast-bottom-right'
|
||||||
|
ref='toaster'
|
||||||
|
toastMessageFactory={ toastMessageFactory } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapComponent = compose(
|
||||||
|
// connect Component to Redux Store
|
||||||
|
connect(mapStateToProps, { fetchUser }),
|
||||||
|
// handles prefetching data
|
||||||
|
contain(fetchContainerOptions)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default wrapComponent(FreeCodeCamp);
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import { Cat } from 'thundercats';
|
|
||||||
import stamp from 'stampit';
|
|
||||||
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 from './routes/Jobs/flux';
|
|
||||||
|
|
||||||
const ajaxStamp = stamp({
|
|
||||||
methods: {
|
|
||||||
postJSON$,
|
|
||||||
post$
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Cat().init(({ instance: cat, args: [services] }) => {
|
|
||||||
const serviceStamp = stamp({
|
|
||||||
methods: {
|
|
||||||
readService$(resource, params, config) {
|
|
||||||
|
|
||||||
return Observable.create(function(observer) {
|
|
||||||
services.read(resource, params, config, (err, res) => {
|
|
||||||
if (err) {
|
|
||||||
return observer.onError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.onNext(res);
|
|
||||||
observer.onCompleted();
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
@ -1,17 +0,0 @@
|
|||||||
import Rx from 'rx';
|
|
||||||
import { match } from 'react-router';
|
|
||||||
import App from './App.jsx';
|
|
||||||
import AppCat from './Cat';
|
|
||||||
|
|
||||||
import childRoutes from './routes';
|
|
||||||
|
|
||||||
const route$ = Rx.Observable.fromNodeCallback(match);
|
|
||||||
|
|
||||||
const routes = Object.assign({ components: App }, childRoutes);
|
|
||||||
|
|
||||||
export default function app$({ location, history }) {
|
|
||||||
return route$({ routes, location, history })
|
|
||||||
.map(([nextLocation, props]) => {
|
|
||||||
return { nextLocation, props, AppCat };
|
|
||||||
});
|
|
||||||
}
|
|
1
common/app/components/Footer/README.md
Normal file
1
common/app/components/Footer/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Currently not used
|
@ -23,20 +23,20 @@ const logoElement = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const toggleButtonChild = (
|
const toggleButtonChild = (
|
||||||
<Col xs={ 12 }>
|
<Col xs={ 12 }>
|
||||||
<span className='hamburger-text'>Menu</span>
|
<span className='hamburger-text'>Menu</span>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default React.createClass({
|
export default class extends React.Component {
|
||||||
displayName: 'Nav',
|
static displayName = 'Nav';
|
||||||
|
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
signedIn: PropTypes.bool,
|
signedIn: PropTypes.bool,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
},
|
};
|
||||||
|
|
||||||
renderLinks() {
|
renderLinks() {
|
||||||
return navLinks.map(({ content, link, react, target }, index) => {
|
return navLinks.map(({ content, link, react, target }, index) => {
|
||||||
@ -63,7 +63,7 @@ export default React.createClass({
|
|||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
renderPoints(username, points) {
|
renderPoints(username, points) {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
@ -76,7 +76,7 @@ export default React.createClass({
|
|||||||
[ { points } ]
|
[ { points } ]
|
||||||
</FCCNavItem>
|
</FCCNavItem>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
renderSignin(username, picture) {
|
renderSignin(username, picture) {
|
||||||
if (username) {
|
if (username) {
|
||||||
@ -100,7 +100,7 @@ export default React.createClass({
|
|||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { username, points, picture } = this.props;
|
const { username, points, picture } = this.props;
|
||||||
@ -124,4 +124,4 @@ export default React.createClass({
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
@ -6,17 +6,21 @@ function goToServer(path) {
|
|||||||
win.location = '/' + path;
|
win.location = '/' + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.createClass({
|
export default class extends React.Component {
|
||||||
displayName: 'NotFound',
|
static displayName = 'NotFound';
|
||||||
propTypes: {
|
|
||||||
|
static propTypes = {
|
||||||
params: PropTypes.object
|
params: PropTypes.object
|
||||||
},
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
goToServer(this.props.params.splat);
|
goToServer(this.props.params.splat);
|
||||||
},
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
},
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <span></span>;
|
return <span></span>;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
79
common/app/create-app.jsx
Normal file
79
common/app/create-app.jsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { match } from 'react-router';
|
||||||
|
import { compose, createStore, applyMiddleware } from 'redux';
|
||||||
|
|
||||||
|
// main app
|
||||||
|
import App from './App.jsx';
|
||||||
|
// app routes
|
||||||
|
import childRoutes from './routes';
|
||||||
|
|
||||||
|
// redux
|
||||||
|
import createReducer from './create-reducer';
|
||||||
|
import middlewares from './middlewares';
|
||||||
|
import sagas from './sagas';
|
||||||
|
|
||||||
|
// general utils
|
||||||
|
import servicesCreator from '../utils/services-creator';
|
||||||
|
|
||||||
|
const createRouteProps = Observable.fromNodeCallback(match);
|
||||||
|
|
||||||
|
const routes = { components: App, ...childRoutes };
|
||||||
|
|
||||||
|
//
|
||||||
|
// createApp(settings: {
|
||||||
|
// location?: Location|String,
|
||||||
|
// history?: History,
|
||||||
|
// initialState?: Object|Void,
|
||||||
|
// serviceOptions?: Object,
|
||||||
|
// middlewares?: Function[],
|
||||||
|
// sideReducers?: Object
|
||||||
|
// enhancers?: Function[],
|
||||||
|
// sagas?: Function[],
|
||||||
|
// }) => Observable
|
||||||
|
//
|
||||||
|
// Either location or history must be defined
|
||||||
|
export default function createApp({
|
||||||
|
location,
|
||||||
|
history,
|
||||||
|
initialState,
|
||||||
|
serviceOptions = {},
|
||||||
|
middlewares: sideMiddlewares = [],
|
||||||
|
enhancers: sideEnhancers = [],
|
||||||
|
reducers: sideReducers = {},
|
||||||
|
sagas: sideSagas = []
|
||||||
|
}) {
|
||||||
|
const sagaOptions = {
|
||||||
|
services: servicesCreator(null, serviceOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhancers = [
|
||||||
|
applyMiddleware(
|
||||||
|
...middlewares,
|
||||||
|
...sideMiddlewares,
|
||||||
|
...[ ...sagas, ...sideSagas].map(saga => saga(sagaOptions)),
|
||||||
|
),
|
||||||
|
// enhancers must come after middlewares
|
||||||
|
// on client side these are things like Redux DevTools
|
||||||
|
...sideEnhancers
|
||||||
|
];
|
||||||
|
const reducer = createReducer(sideReducers);
|
||||||
|
|
||||||
|
// create composed store enhancer
|
||||||
|
// use store enhancer function to enhance `createStore` function
|
||||||
|
// call enhanced createStore function with reducer and initialState
|
||||||
|
// to create store
|
||||||
|
const store = compose(...enhancers)(createStore)(reducer, initialState);
|
||||||
|
|
||||||
|
// createRouteProps({
|
||||||
|
// redirect: LocationDescriptor,
|
||||||
|
// history: History,
|
||||||
|
// routes: Object
|
||||||
|
// }) => Observable
|
||||||
|
return createRouteProps({ routes, location, history })
|
||||||
|
.map(([ redirect, props ]) => ({
|
||||||
|
redirect,
|
||||||
|
props,
|
||||||
|
reducer,
|
||||||
|
store
|
||||||
|
}));
|
||||||
|
}
|
19
common/app/create-reducer.js
Normal file
19
common/app/create-reducer.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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';
|
||||||
|
import {
|
||||||
|
reducer as jobsApp,
|
||||||
|
formNormalizer as jobsNormalizer
|
||||||
|
} from './routes/Jobs/redux';
|
||||||
|
|
||||||
|
export default function createReducer(sideReducers = {}) {
|
||||||
|
return combineReducers({
|
||||||
|
...sideReducers,
|
||||||
|
app,
|
||||||
|
hikesApp,
|
||||||
|
jobsApp,
|
||||||
|
form: formReducer.normalize(jobsNormalizer)
|
||||||
|
});
|
||||||
|
}
|
@ -1,103 +0,0 @@
|
|||||||
import { Store } from 'thundercats';
|
|
||||||
|
|
||||||
const { createRegistrar, setter, fromMany } = Store;
|
|
||||||
const initValue = {
|
|
||||||
title: 'Learn To Code | Free Code Camp',
|
|
||||||
username: null,
|
|
||||||
picture: null,
|
|
||||||
points: 0,
|
|
||||||
hikesApp: {
|
|
||||||
hikes: [],
|
|
||||||
// lecture state
|
|
||||||
currentHike: {},
|
|
||||||
showQuestions: false
|
|
||||||
},
|
|
||||||
jobsApp: {
|
|
||||||
showModal: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Store({
|
|
||||||
refs: {
|
|
||||||
displayName: 'AppStore',
|
|
||||||
value: initValue
|
|
||||||
},
|
|
||||||
init({ instance: store, args: [cat] }) {
|
|
||||||
const register = createRegistrar(store);
|
|
||||||
// app
|
|
||||||
const {
|
|
||||||
updateLocation,
|
|
||||||
getUser,
|
|
||||||
setTitle,
|
|
||||||
toast
|
|
||||||
} = cat.getActions('appActions');
|
|
||||||
|
|
||||||
register(
|
|
||||||
fromMany(
|
|
||||||
setter(
|
|
||||||
fromMany(
|
|
||||||
getUser,
|
|
||||||
setTitle
|
|
||||||
)
|
|
||||||
),
|
|
||||||
updateLocation,
|
|
||||||
toast
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// hikes
|
|
||||||
const {
|
|
||||||
toggleQuestions,
|
|
||||||
fetchHikes,
|
|
||||||
resetHike,
|
|
||||||
grabQuestion,
|
|
||||||
releaseQuestion,
|
|
||||||
moveQuestion,
|
|
||||||
answer
|
|
||||||
} = cat.getActions('hikesActions');
|
|
||||||
|
|
||||||
register(
|
|
||||||
fromMany(
|
|
||||||
toggleQuestions,
|
|
||||||
fetchHikes,
|
|
||||||
resetHike,
|
|
||||||
grabQuestion,
|
|
||||||
releaseQuestion,
|
|
||||||
moveQuestion,
|
|
||||||
answer
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,2 +0,0 @@
|
|||||||
export AppActions from './Actions';
|
|
||||||
export AppStore from './Store';
|
|
@ -1 +1 @@
|
|||||||
export default from './app-stream.jsx';
|
export default from './create-app.jsx';
|
||||||
|
0
common/app/middlewares.js
Normal file
0
common/app/middlewares.js
Normal file
11
common/app/provide-store.js
Normal file
11
common/app/provide-store.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/* eslint-disable react/display-name */
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
export default function provideStore(element, store) {
|
||||||
|
return React.createElement(
|
||||||
|
Provider,
|
||||||
|
{ store },
|
||||||
|
element
|
||||||
|
);
|
||||||
|
}
|
32
common/app/redux/actions.js
Normal file
32
common/app/redux/actions.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import types from './types';
|
||||||
|
|
||||||
|
// updateTitle(title: String) => Action
|
||||||
|
export const updateTitle = createAction(types.updateTitle);
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
// makeToast({ type?: String, message: String, title: String }) => Action
|
||||||
|
export const makeToast = createAction(
|
||||||
|
types.makeToast,
|
||||||
|
toast => {
|
||||||
|
id += 1;
|
||||||
|
return {
|
||||||
|
...toast,
|
||||||
|
id,
|
||||||
|
type: toast.type || 'info'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// fetchUser() => Action
|
||||||
|
// used in combination with fetch-user-saga
|
||||||
|
export const fetchUser = createAction(types.fetchUser);
|
||||||
|
|
||||||
|
// setUser(userInfo: Object) => Action
|
||||||
|
export const setUser = createAction(types.setUser);
|
||||||
|
|
||||||
|
// updatePoints(points: Number) => Action
|
||||||
|
export const updatePoints = createAction(types.updatePoints);
|
||||||
|
|
||||||
|
// hardGoTo(path: String) => Action
|
||||||
|
export const hardGoTo = createAction(types.hardGoTo);
|
39
common/app/redux/fetch-user-saga.js
Normal file
39
common/app/redux/fetch-user-saga.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { handleError, setUser, fetchUser } from './types';
|
||||||
|
|
||||||
|
export default ({ services }) => ({ dispatch }) => next => {
|
||||||
|
return function getUserSaga(action) {
|
||||||
|
if (action.type !== fetchUser) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
return services.readService$({ service: 'user' })
|
||||||
|
.map(({
|
||||||
|
username,
|
||||||
|
picture,
|
||||||
|
progressTimestamps = [],
|
||||||
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
|
isFullStackCert
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
type: setUser,
|
||||||
|
payload: {
|
||||||
|
username,
|
||||||
|
picture,
|
||||||
|
points: progressTimestamps.length,
|
||||||
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
|
isFullStackCert,
|
||||||
|
isSignedIn: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => Observable.just({
|
||||||
|
type: handleError,
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
6
common/app/redux/index.js
Normal file
6
common/app/redux/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { default as reducer } from './reducer';
|
||||||
|
export { default as actions } from './actions';
|
||||||
|
export { default as types } from './types';
|
||||||
|
|
||||||
|
import fetchUserSaga from './fetch-user-saga';
|
||||||
|
export const sagas = [ fetchUserSaga ];
|
35
common/app/redux/reducer.js
Normal file
35
common/app/redux/reducer.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
import types from './types';
|
||||||
|
|
||||||
|
export default handleActions(
|
||||||
|
{
|
||||||
|
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
||||||
|
...state,
|
||||||
|
title: payload + ' | Free Code Camp'
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.makeToast]: (state, { payload: toast }) => ({
|
||||||
|
...state,
|
||||||
|
toast
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.setUser]: (state, { payload: user }) => ({ ...state, ...user }),
|
||||||
|
|
||||||
|
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
||||||
|
...state,
|
||||||
|
points
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.updatePoints]: (state, { payload: points }) => ({
|
||||||
|
...state,
|
||||||
|
points
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Learn To Code | Free Code Camp',
|
||||||
|
username: null,
|
||||||
|
picture: null,
|
||||||
|
points: 0,
|
||||||
|
isSignedIn: false
|
||||||
|
}
|
||||||
|
);
|
14
common/app/redux/types.js
Normal file
14
common/app/redux/types.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import createTypes from '../utils/create-types';
|
||||||
|
|
||||||
|
export default createTypes([
|
||||||
|
'updateTitle',
|
||||||
|
|
||||||
|
'fetchUser',
|
||||||
|
'setUser',
|
||||||
|
|
||||||
|
'makeToast',
|
||||||
|
'updatePoints',
|
||||||
|
'handleError',
|
||||||
|
// used to hit the server
|
||||||
|
'hardGoTo'
|
||||||
|
], 'app');
|
@ -1 +0,0 @@
|
|||||||
future home of FAVS app
|
|
@ -1,63 +1,76 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { contain } from 'thundercats-react';
|
import { connect } from 'react-redux';
|
||||||
import { Col, Row } from 'react-bootstrap';
|
import { Col, Row } from 'react-bootstrap';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import Lecture from './Lecture.jsx';
|
import Lecture from './Lecture.jsx';
|
||||||
import Questions from './Questions.jsx';
|
import Questions from './Questions.jsx';
|
||||||
|
import { resetHike } from '../redux/actions';
|
||||||
|
import { getCurrentHike } from '../redux/selectors';
|
||||||
|
|
||||||
export default contain(
|
const mapStateToProps = createSelector(
|
||||||
{
|
getCurrentHike,
|
||||||
actions: ['hikesActions']
|
state => state.hikesApp.shouldShowQuestions,
|
||||||
},
|
(currentHike, shouldShowQuestions) => ({
|
||||||
React.createClass({
|
title: currentHike ? currentHike.title : '',
|
||||||
displayName: 'Hike',
|
shouldShowQuestions
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
currentHike: PropTypes.object,
|
|
||||||
hikesActions: PropTypes.object,
|
|
||||||
params: PropTypes.object,
|
|
||||||
showQuestions: PropTypes.bool
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.hikesActions.resetHike();
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps({ params: { dashedName } }) {
|
|
||||||
if (this.props.params.dashedName !== dashedName) {
|
|
||||||
this.props.hikesActions.resetHike();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderBody(showQuestions) {
|
|
||||||
if (showQuestions) {
|
|
||||||
return <Questions />;
|
|
||||||
}
|
|
||||||
return <Lecture />;
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
currentHike: { title } = {},
|
|
||||||
showQuestions
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Row>
|
|
||||||
<header className='text-center'>
|
|
||||||
<h4>{ title }</h4>
|
|
||||||
</header>
|
|
||||||
<hr />
|
|
||||||
<div className='spacer' />
|
|
||||||
<section
|
|
||||||
className={ 'text-center' }
|
|
||||||
title={ title }>
|
|
||||||
{ this.renderBody(showQuestions) }
|
|
||||||
</section>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// export plain component for testing
|
||||||
|
export class Hike extends React.Component {
|
||||||
|
static displayName = 'Hike';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
// actions
|
||||||
|
resetHike: PropTypes.func,
|
||||||
|
// ui
|
||||||
|
title: PropTypes.string,
|
||||||
|
params: PropTypes.object,
|
||||||
|
shouldShowQuestions: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.resetHike();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps({ params: { dashedName } }) {
|
||||||
|
if (this.props.params.dashedName !== dashedName) {
|
||||||
|
this.props.resetHike();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(showQuestions) {
|
||||||
|
if (showQuestions) {
|
||||||
|
return <Questions />;
|
||||||
|
}
|
||||||
|
return <Lecture />;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
shouldShowQuestions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={ 12 }>
|
||||||
|
<Row>
|
||||||
|
<header className='text-center'>
|
||||||
|
<h4>{ title }</h4>
|
||||||
|
</header>
|
||||||
|
<hr />
|
||||||
|
<div className='spacer' />
|
||||||
|
<section
|
||||||
|
className={ 'text-center' }
|
||||||
|
title={ title }>
|
||||||
|
{ this.renderBody(shouldShowQuestions) }
|
||||||
|
</section>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export redux aware component
|
||||||
|
export default connect(mapStateToProps, { resetHike })(Hike);
|
||||||
|
@ -1,74 +1,82 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { Row } from 'react-bootstrap';
|
import { Row } from 'react-bootstrap';
|
||||||
import { contain } from 'thundercats-react';
|
import PureComponent from 'react-pure-render/component';
|
||||||
// import debugFactory from 'debug';
|
import { createSelector } from 'reselect';
|
||||||
|
// import debug from 'debug';
|
||||||
|
|
||||||
import HikesMap from './Map.jsx';
|
import HikesMap from './Map.jsx';
|
||||||
|
import { updateTitle } from '../../../redux/actions';
|
||||||
|
import { fetchHikes } from '../redux/actions';
|
||||||
|
|
||||||
// const debug = debugFactory('freecc:hikes');
|
import contain from '../../../utils/professor-x';
|
||||||
|
|
||||||
export default contain(
|
// const log = debug('fcc:hikes');
|
||||||
{
|
|
||||||
store: 'appStore',
|
const mapStateToProps = createSelector(
|
||||||
map(state) {
|
state => state.hikesApp.hikes.entities,
|
||||||
return state.hikesApp;
|
state => state.hikesApp.hikes.results,
|
||||||
},
|
(hikesMap, hikesByDashedName)=> {
|
||||||
actions: ['appActions'],
|
if (!hikesMap || !hikesByDashedName) {
|
||||||
fetchAction: 'hikesActions.fetchHikes',
|
return { hikes: [] };
|
||||||
getPayload: ({ hikes, params }) => ({
|
|
||||||
isPrimed: (hikes && !!hikes.length),
|
|
||||||
dashedName: params.dashedName
|
|
||||||
}),
|
|
||||||
shouldContainerFetch(props, nextProps) {
|
|
||||||
return props.params.dashedName !== nextProps.params.dashedName;
|
|
||||||
}
|
}
|
||||||
},
|
return {
|
||||||
React.createClass({
|
hikes: hikesByDashedName.map(dashedName => hikesMap[dashedName])
|
||||||
displayName: 'Hikes',
|
};
|
||||||
|
}
|
||||||
propTypes: {
|
|
||||||
appActions: PropTypes.object,
|
|
||||||
children: PropTypes.element,
|
|
||||||
currentHike: PropTypes.object,
|
|
||||||
hikes: PropTypes.array,
|
|
||||||
params: PropTypes.object,
|
|
||||||
showQuestions: PropTypes.bool
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
const { appActions } = this.props;
|
|
||||||
appActions.setTitle('Videos');
|
|
||||||
},
|
|
||||||
|
|
||||||
renderMap(hikes) {
|
|
||||||
return (
|
|
||||||
<HikesMap hikes={ hikes }/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderChild({ children, ...props }) {
|
|
||||||
if (!children) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return React.cloneElement(children, props);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { hikes } = this.props;
|
|
||||||
const { dashedName } = this.props.params;
|
|
||||||
const preventOverflow = { overflow: 'hidden' };
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row style={ preventOverflow }>
|
|
||||||
{
|
|
||||||
// render sub-route
|
|
||||||
this.renderChild({ ...this.props, dashedName }) ||
|
|
||||||
// if no sub-route render hikes map
|
|
||||||
this.renderMap(hikes)
|
|
||||||
}
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fetchOptions = {
|
||||||
|
fetchAction: 'fetchHikes',
|
||||||
|
isPrimed: ({ hikes }) => hikes && !!hikes.length,
|
||||||
|
getActionArgs: ({ params: { dashedName } }) => [ dashedName ],
|
||||||
|
shouldContainerFetch(props, nextProps) {
|
||||||
|
return props.params.dashedName !== nextProps.params.dashedName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Hikes extends PureComponent {
|
||||||
|
static displayName = 'Hikes';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.element,
|
||||||
|
hikes: PropTypes.array,
|
||||||
|
params: PropTypes.object,
|
||||||
|
updateTitle: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const { updateTitle } = this.props;
|
||||||
|
updateTitle('Hikes');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMap(hikes) {
|
||||||
|
return (
|
||||||
|
<HikesMap hikes={ hikes }/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hikes } = this.props;
|
||||||
|
const preventOverflow = { overflow: 'hidden' };
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row style={ preventOverflow }>
|
||||||
|
{
|
||||||
|
// render sub-route
|
||||||
|
this.props.children ||
|
||||||
|
// if no sub-route render hikes map
|
||||||
|
this.renderMap(hikes)
|
||||||
|
}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export redux and fetch aware component
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps, { fetchHikes, updateTitle }),
|
||||||
|
contain(fetchOptions)
|
||||||
|
)(Hikes);
|
||||||
|
@ -1,95 +1,103 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { contain } from 'thundercats-react';
|
import { connect } from 'react-redux';
|
||||||
import { Button, Col, Row } from 'react-bootstrap';
|
import { Button, Col, Row } from 'react-bootstrap';
|
||||||
import { History } from 'react-router';
|
|
||||||
import Vimeo from 'react-vimeo';
|
import Vimeo from 'react-vimeo';
|
||||||
import debugFactory from 'debug';
|
import { createSelector } from 'reselect';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:hikes');
|
import { hardGoTo } from '../../../redux/actions';
|
||||||
|
import { toggleQuestionView } from '../redux/actions';
|
||||||
|
import { getCurrentHike } from '../redux/selectors';
|
||||||
|
|
||||||
export default contain(
|
const log = debug('fcc:hikes');
|
||||||
{
|
|
||||||
actions: ['hikesActions'],
|
|
||||||
store: 'appStore',
|
|
||||||
map(state) {
|
|
||||||
const {
|
|
||||||
currentHike: {
|
|
||||||
dashedName,
|
|
||||||
description,
|
|
||||||
challengeSeed: [id] = [0]
|
|
||||||
} = {}
|
|
||||||
} = state.hikesApp;
|
|
||||||
|
|
||||||
return {
|
const mapStateToProps = createSelector(
|
||||||
dashedName,
|
getCurrentHike,
|
||||||
description,
|
(currentHike) => {
|
||||||
id
|
const {
|
||||||
};
|
dashedName,
|
||||||
}
|
description,
|
||||||
},
|
challengeSeed: [id] = [0]
|
||||||
React.createClass({
|
} = currentHike;
|
||||||
displayName: 'Lecture',
|
return {
|
||||||
mixins: [History],
|
id,
|
||||||
|
dashedName,
|
||||||
propTypes: {
|
description
|
||||||
dashedName: PropTypes.string,
|
};
|
||||||
description: PropTypes.array,
|
}
|
||||||
id: PropTypes.string,
|
|
||||||
hikesActions: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
|
||||||
const { props } = this;
|
|
||||||
return nextProps.id !== props.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleError: debug,
|
|
||||||
|
|
||||||
handleFinish(hikesActions) {
|
|
||||||
debug('loading questions');
|
|
||||||
hikesActions.toggleQuestions();
|
|
||||||
},
|
|
||||||
|
|
||||||
renderTranscript(transcript, dashedName) {
|
|
||||||
return transcript.map((line, index) => (
|
|
||||||
<p
|
|
||||||
className='lead text-left'
|
|
||||||
key={ dashedName + index }>
|
|
||||||
{ line }
|
|
||||||
</p>
|
|
||||||
));
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id = '1',
|
|
||||||
description = [],
|
|
||||||
hikesActions
|
|
||||||
} = this.props;
|
|
||||||
const dashedName = 'foo';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Row>
|
|
||||||
<Vimeo
|
|
||||||
onError={ this.handleError }
|
|
||||||
onFinish= { () => this.handleFinish(hikesActions) }
|
|
||||||
videoId={ id } />
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<article>
|
|
||||||
{ this.renderTranscript(description, dashedName) }
|
|
||||||
</article>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='primary'
|
|
||||||
onClick={ () => this.handleFinish(hikesActions) }>
|
|
||||||
Take me to the Questions
|
|
||||||
</Button>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export class Lecture extends React.Component {
|
||||||
|
static displayName = 'Lecture';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
// actions
|
||||||
|
toggleQuestionView: PropTypes.func,
|
||||||
|
// ui
|
||||||
|
id: PropTypes.number,
|
||||||
|
description: PropTypes.array,
|
||||||
|
dashedName: PropTypes.string,
|
||||||
|
hardGoTo: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
if (!this.props.id) {
|
||||||
|
this.props.hardGoTo('/map');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
const { props } = this;
|
||||||
|
return nextProps.id !== props.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError: log;
|
||||||
|
|
||||||
|
renderTranscript(transcript, dashedName) {
|
||||||
|
return transcript.map((line, index) => (
|
||||||
|
<p
|
||||||
|
className='lead text-left'
|
||||||
|
key={ dashedName + index }>
|
||||||
|
{ line }
|
||||||
|
</p>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id = '1',
|
||||||
|
description = [],
|
||||||
|
toggleQuestionView
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const dashedName = 'foo';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={ 12 }>
|
||||||
|
<Row>
|
||||||
|
<Vimeo
|
||||||
|
onError={ this.handleError }
|
||||||
|
onFinish= { toggleQuestionView }
|
||||||
|
videoId={ '' + id } />
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<article>
|
||||||
|
{ this.renderTranscript(description, dashedName) }
|
||||||
|
</article>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={ toggleQuestionView }>
|
||||||
|
Take me to the Questions
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ hardGoTo, toggleQuestionView }
|
||||||
|
)(Lecture);
|
||||||
|
@ -14,10 +14,10 @@ export default React.createClass({
|
|||||||
hikes = [{}]
|
hikes = [{}]
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const vidElements = hikes.map(({ title, dashedName}) => {
|
const vidElements = hikes.map(({ title, dashedName }) => {
|
||||||
return (
|
return (
|
||||||
<ListGroupItem key={ dashedName }>
|
<ListGroupItem key={ dashedName }>
|
||||||
<Link to={ `/hikes/${dashedName}` }>
|
<Link to={ `/videos/${dashedName}` }>
|
||||||
<h3>{ title }</h3>
|
<h3>{ title }</h3>
|
||||||
</Link>
|
</Link>
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
|
@ -1,177 +1,212 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { spring, Motion } from 'react-motion';
|
import { spring, Motion } from 'react-motion';
|
||||||
import { contain } from 'thundercats-react';
|
import { connect } from 'react-redux';
|
||||||
import { Button, Col, Row } from 'react-bootstrap';
|
import { Button, Col, Row } from 'react-bootstrap';
|
||||||
|
import { CompositeDisposable } from 'rx';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import {
|
||||||
|
answerQuestion,
|
||||||
|
moveQuestion,
|
||||||
|
releaseQuestion,
|
||||||
|
grabQuestion
|
||||||
|
} from '../redux/actions';
|
||||||
|
import { getCurrentHike } from '../redux/selectors';
|
||||||
|
|
||||||
const answerThreshold = 100;
|
const answerThreshold = 100;
|
||||||
|
const springProperties = { stiffness: 120, damping: 10 };
|
||||||
|
const actionsToBind = {
|
||||||
|
answerQuestion,
|
||||||
|
moveQuestion,
|
||||||
|
releaseQuestion,
|
||||||
|
grabQuestion
|
||||||
|
};
|
||||||
|
|
||||||
export default contain(
|
const mapStateToProps = createSelector(
|
||||||
{
|
getCurrentHike,
|
||||||
store: 'appStore',
|
state => state.hikesApp,
|
||||||
actions: ['hikesActions'],
|
state => state.app.isSignedIn,
|
||||||
map({ hikesApp, username }) {
|
(currentHike, ui, isSignedIn) => {
|
||||||
const {
|
const {
|
||||||
currentHike,
|
currentQuestion = 1,
|
||||||
currentQuestion = 1,
|
mouse = [ 0, 0 ],
|
||||||
mouse = [0, 0],
|
delta = [ 0, 0 ],
|
||||||
isCorrect = false,
|
isCorrect = false,
|
||||||
delta = [0, 0],
|
isPressed = false,
|
||||||
isPressed = false,
|
shouldShakeQuestion = false
|
||||||
shake = false
|
} = ui;
|
||||||
} = hikesApp;
|
|
||||||
return {
|
|
||||||
hike: currentHike,
|
|
||||||
currentQuestion,
|
|
||||||
mouse,
|
|
||||||
isCorrect,
|
|
||||||
delta,
|
|
||||||
isPressed,
|
|
||||||
shake,
|
|
||||||
isSignedIn: !!username
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
React.createClass({
|
|
||||||
displayName: 'Questions',
|
|
||||||
|
|
||||||
propTypes: {
|
const {
|
||||||
hike: PropTypes.object,
|
tests = []
|
||||||
currentQuestion: PropTypes.number,
|
} = currentHike;
|
||||||
mouse: PropTypes.array,
|
|
||||||
isCorrect: PropTypes.bool,
|
|
||||||
delta: PropTypes.array,
|
|
||||||
isPressed: PropTypes.bool,
|
|
||||||
shake: PropTypes.bool,
|
|
||||||
isSignedIn: PropTypes.bool,
|
|
||||||
hikesActions: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMouseUp(e, answer, info) {
|
return {
|
||||||
e.stopPropagation();
|
tests,
|
||||||
if (!this.props.isPressed) {
|
currentQuestion,
|
||||||
return null;
|
isCorrect,
|
||||||
}
|
mouse,
|
||||||
|
delta,
|
||||||
const {
|
isPressed,
|
||||||
hike,
|
shouldShakeQuestion,
|
||||||
currentQuestion,
|
isSignedIn
|
||||||
isSignedIn,
|
};
|
||||||
delta
|
}
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.hikesActions.releaseQuestion();
|
|
||||||
this.props.hikesActions.answer({
|
|
||||||
e,
|
|
||||||
answer,
|
|
||||||
hike,
|
|
||||||
delta,
|
|
||||||
currentQuestion,
|
|
||||||
isSignedIn,
|
|
||||||
info,
|
|
||||||
threshold: answerThreshold
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMouseMove(e) {
|
|
||||||
if (!this.props.isPressed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { delta, hikesActions } = this.props;
|
|
||||||
|
|
||||||
hikesActions.moveQuestion({ e, delta });
|
|
||||||
},
|
|
||||||
|
|
||||||
onAnswer(answer, userAnswer, info) {
|
|
||||||
const { isSignedIn, hike, currentQuestion, hikesActions } = this.props;
|
|
||||||
return (e) => {
|
|
||||||
if (e && e.preventDefault) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
return hikesActions.answer({
|
|
||||||
answer,
|
|
||||||
userAnswer,
|
|
||||||
currentQuestion,
|
|
||||||
hike,
|
|
||||||
info,
|
|
||||||
isSignedIn
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
renderQuestion(number, question, answer, shake, info) {
|
|
||||||
const { hikesActions } = this.props;
|
|
||||||
const mouseUp = e => this.handleMouseUp(e, answer, info);
|
|
||||||
return ({ x }) => {
|
|
||||||
const style = {
|
|
||||||
WebkitTransform: `translate3d(${ x }px, 0, 0)`,
|
|
||||||
transform: `translate3d(${ x }px, 0, 0)`
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className={ shake ? 'animated swing shake' : '' }
|
|
||||||
onMouseDown={ hikesActions.grabQuestion }
|
|
||||||
onMouseLeave={ mouseUp }
|
|
||||||
onMouseMove={ this.handleMouseMove }
|
|
||||||
onMouseUp={ mouseUp }
|
|
||||||
onTouchEnd={ mouseUp }
|
|
||||||
onTouchMove={ this.handleMouseMove }
|
|
||||||
onTouchStart={ hikesActions.grabQuestion }
|
|
||||||
style={ style }>
|
|
||||||
<h4>Question { number }</h4>
|
|
||||||
<p>{ question }</p>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
hike: { tests = [] } = {},
|
|
||||||
mouse: [x],
|
|
||||||
currentQuestion,
|
|
||||||
shake
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const [ question, answer, info ] = tests[currentQuestion - 1] || [];
|
|
||||||
const questionElement = this.renderQuestion(
|
|
||||||
currentQuestion,
|
|
||||||
question,
|
|
||||||
answer,
|
|
||||||
shake,
|
|
||||||
info
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col
|
|
||||||
onMouseUp={ e => this.handleMouseUp(e, answer, info) }
|
|
||||||
xs={ 8 }
|
|
||||||
xsOffset={ 2 }>
|
|
||||||
<Row>
|
|
||||||
<Motion style={{ x: spring(x, { stiffness: 120, damping: 10 }) }}>
|
|
||||||
{ questionElement }
|
|
||||||
</Motion>
|
|
||||||
<div className='spacer' />
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='pull-left'
|
|
||||||
onClick={ this.onAnswer(answer, false, info) }>
|
|
||||||
false
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='pull-right'
|
|
||||||
onClick={ this.onAnswer(answer, true, info) }>
|
|
||||||
true
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class Question extends React.Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this._subscriptions = new CompositeDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
static displayName = 'Questions';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
// actions
|
||||||
|
answerQuestion: PropTypes.func,
|
||||||
|
releaseQuestion: PropTypes.func,
|
||||||
|
moveQuestion: PropTypes.func,
|
||||||
|
grabQuestion: PropTypes.func,
|
||||||
|
// ui state
|
||||||
|
tests: PropTypes.array,
|
||||||
|
mouse: PropTypes.array,
|
||||||
|
delta: PropTypes.array,
|
||||||
|
isCorrect: PropTypes.bool,
|
||||||
|
isPressed: PropTypes.bool,
|
||||||
|
isSignedIn: PropTypes.bool,
|
||||||
|
currentQuestion: PropTypes.number,
|
||||||
|
shouldShakeQuestion: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._subscriptions.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseUp(e, answer, info) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!this.props.isPressed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
releaseQuestion,
|
||||||
|
answerQuestion
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
releaseQuestion();
|
||||||
|
const subscription = answerQuestion({
|
||||||
|
e,
|
||||||
|
answer,
|
||||||
|
info,
|
||||||
|
threshold: answerThreshold
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this._subscriptions.add(subscription);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove(isPressed, { delta, moveQuestion }) {
|
||||||
|
if (!isPressed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return e => moveQuestion({ e, delta });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAnswer(answer, userAnswer, info) {
|
||||||
|
const { isSignedIn, answerQuestion } = this.props;
|
||||||
|
const subscriptions = this._subscriptions;
|
||||||
|
return e => {
|
||||||
|
if (e && e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = answerQuestion({
|
||||||
|
answer,
|
||||||
|
userAnswer,
|
||||||
|
info,
|
||||||
|
isSignedIn
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
subscriptions.add(subscription);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderQuestion(number, question, answer, shouldShakeQuestion, info) {
|
||||||
|
const { grabQuestion, isPressed } = this.props;
|
||||||
|
const mouseUp = e => this.handleMouseUp(e, answer, info);
|
||||||
|
return ({ x }) => {
|
||||||
|
const style = {
|
||||||
|
WebkitTransform: `translate3d(${ x }px, 0, 0)`,
|
||||||
|
transform: `translate3d(${ x }px, 0, 0)`
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={ shouldShakeQuestion ? 'animated swing shake' : '' }
|
||||||
|
onMouseDown={ grabQuestion }
|
||||||
|
onMouseLeave={ mouseUp }
|
||||||
|
onMouseMove={ this.handleMouseMove(isPressed, this.props) }
|
||||||
|
onMouseUp={ mouseUp }
|
||||||
|
onTouchEnd={ mouseUp }
|
||||||
|
onTouchMove={ this.handleMouseMove(isPressed, this.props) }
|
||||||
|
onTouchStart={ grabQuestion }
|
||||||
|
style={ style }>
|
||||||
|
<h4>Question { number }</h4>
|
||||||
|
<p>{ question }</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
tests = [],
|
||||||
|
mouse: [xPosition],
|
||||||
|
currentQuestion,
|
||||||
|
shouldShakeQuestion
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const [ question, answer, info ] = tests[currentQuestion - 1] || [];
|
||||||
|
const questionElement = this.renderQuestion(
|
||||||
|
currentQuestion,
|
||||||
|
question,
|
||||||
|
answer,
|
||||||
|
shouldShakeQuestion,
|
||||||
|
info
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
onMouseUp={ e => this.handleMouseUp(e, answer, info) }
|
||||||
|
xs={ 8 }
|
||||||
|
xsOffset={ 2 }>
|
||||||
|
<Row>
|
||||||
|
<Motion style={{ x: spring(xPosition, springProperties) }}>
|
||||||
|
{ questionElement }
|
||||||
|
</Motion>
|
||||||
|
<div className='spacer' />
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
className='pull-left'
|
||||||
|
onClick={ this.onAnswer(answer, false, info) }>
|
||||||
|
false
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
className='pull-right'
|
||||||
|
onClick={ this.onAnswer(answer, true, info) }>
|
||||||
|
true
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, actionsToBind)(Question);
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default from './Actions';
|
|
@ -1,11 +1,6 @@
|
|||||||
import Hikes from './components/Hikes.jsx';
|
import Hikes from './components/Hikes.jsx';
|
||||||
import Hike from './components/Hike.jsx';
|
import Hike from './components/Hike.jsx';
|
||||||
|
|
||||||
/*
|
|
||||||
* show video /hikes/someVideo
|
|
||||||
* show question /hikes/someVideo/question1
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: 'videos',
|
path: 'videos',
|
||||||
component: Hikes,
|
component: Hikes,
|
||||||
|
57
common/app/routes/Hikes/redux/actions.js
Normal file
57
common/app/routes/Hikes/redux/actions.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
|
||||||
|
import types from './types';
|
||||||
|
import { getMouse } from './utils';
|
||||||
|
|
||||||
|
|
||||||
|
// fetchHikes(dashedName?: String) => Action
|
||||||
|
// used with fetchHikesSaga
|
||||||
|
export const fetchHikes = createAction(types.fetchHikes);
|
||||||
|
|
||||||
|
// fetchHikesCompleted(hikes: Object) => Action
|
||||||
|
// hikes is a normalized response from server
|
||||||
|
// called within fetchHikesSaga
|
||||||
|
export const fetchHikesCompleted = createAction(
|
||||||
|
types.fetchHikesCompleted,
|
||||||
|
(hikes, currentHike) => ({ hikes, currentHike })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resetHike = createAction(types.resetHike);
|
||||||
|
|
||||||
|
export const toggleQuestionView = createAction(types.toggleQuestionView);
|
||||||
|
|
||||||
|
export const grabQuestion = createAction(types.grabQuestion, e => {
|
||||||
|
let { pageX, pageY, touches } = e;
|
||||||
|
if (touches) {
|
||||||
|
e.preventDefault();
|
||||||
|
// these re-assigns the values of pageX, pageY from touches
|
||||||
|
({ pageX, pageY } = touches[0]);
|
||||||
|
}
|
||||||
|
const delta = [pageX, pageY];
|
||||||
|
const mouse = [0, 0];
|
||||||
|
|
||||||
|
return { delta, mouse };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const releaseQuestion = createAction(types.releaseQuestion);
|
||||||
|
export const moveQuestion = createAction(
|
||||||
|
types.moveQuestion,
|
||||||
|
({ e, delta }) => getMouse(e, delta)
|
||||||
|
);
|
||||||
|
|
||||||
|
// answer({
|
||||||
|
// e: Event,
|
||||||
|
// answer: Boolean,
|
||||||
|
// userAnswer: Boolean,
|
||||||
|
// info: String,
|
||||||
|
// threshold: Number
|
||||||
|
// }) => Action
|
||||||
|
export const answerQuestion = createAction(types.answerQuestion);
|
||||||
|
|
||||||
|
export const startShake = createAction(types.startShake);
|
||||||
|
export const endShake = createAction(types.primeNextQuestion);
|
||||||
|
|
||||||
|
export const goToNextQuestion = createAction(types.goToNextQuestion);
|
||||||
|
|
||||||
|
export const hikeCompleted = createAction(types.hikeCompleted);
|
||||||
|
export const goToNextHike = createAction(types.goToNextHike);
|
146
common/app/routes/Hikes/redux/answer-saga.js
Normal file
146
common/app/routes/Hikes/redux/answer-saga.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { push } from 'react-router-redux';
|
||||||
|
|
||||||
|
import types from './types';
|
||||||
|
import { getMouse } from './utils';
|
||||||
|
|
||||||
|
import { makeToast, updatePoints } from '../../../redux/actions';
|
||||||
|
import { hikeCompleted, goToNextHike } from './actions';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
import { getCurrentHike } from './selectors';
|
||||||
|
|
||||||
|
function handleAnswer(getState, dispatch, next, action) {
|
||||||
|
const {
|
||||||
|
e,
|
||||||
|
answer,
|
||||||
|
userAnswer,
|
||||||
|
info,
|
||||||
|
threshold
|
||||||
|
} = action.payload;
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const { id, name, challengeType, tests } = getCurrentHike(state);
|
||||||
|
const {
|
||||||
|
app: { isSignedIn },
|
||||||
|
hikesApp: {
|
||||||
|
currentQuestion,
|
||||||
|
delta = [ 0, 0 ]
|
||||||
|
}
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
let finalAnswer;
|
||||||
|
// drag answer, compute response
|
||||||
|
if (typeof userAnswer === 'undefined') {
|
||||||
|
const [positionX] = getMouse(e, delta);
|
||||||
|
|
||||||
|
// question released under threshold
|
||||||
|
if (Math.abs(positionX) < threshold) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionX >= threshold) {
|
||||||
|
finalAnswer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionX <= -threshold) {
|
||||||
|
finalAnswer = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalAnswer = userAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// incorrect question
|
||||||
|
if (answer !== finalAnswer) {
|
||||||
|
if (info) {
|
||||||
|
dispatch(makeToast({
|
||||||
|
title: 'Hint',
|
||||||
|
message: info,
|
||||||
|
type: 'info'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable
|
||||||
|
.just({ type: types.endShake })
|
||||||
|
.delay(500)
|
||||||
|
.startWith({ type: types.startShake })
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tests[currentQuestion]) {
|
||||||
|
return Observable
|
||||||
|
.just({ type: types.goToNextQuestion })
|
||||||
|
.delay(300)
|
||||||
|
.startWith({ type: types.primeNextQuestion })
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updateUser$;
|
||||||
|
if (isSignedIn) {
|
||||||
|
const body = { id, name, challengeType: +challengeType };
|
||||||
|
updateUser$ = postJSON$('/completed-challenge', body)
|
||||||
|
// if post fails, will retry once
|
||||||
|
.retry(3)
|
||||||
|
.flatMap(({ alreadyCompleted, points }) => {
|
||||||
|
return Observable.of(
|
||||||
|
makeToast({
|
||||||
|
message:
|
||||||
|
'Challenge saved.' +
|
||||||
|
(alreadyCompleted ? '' : ' First time Completed!'),
|
||||||
|
title: 'Saved',
|
||||||
|
type: 'info'
|
||||||
|
}),
|
||||||
|
updatePoints(points),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return Observable.just({
|
||||||
|
type: 'app.error',
|
||||||
|
error
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateUser$ = Observable.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeCompleted$ = Observable.of(
|
||||||
|
goToNextHike(),
|
||||||
|
makeToast({
|
||||||
|
title: 'Congratulations!',
|
||||||
|
message: 'Hike completed.' + (isSignedIn ? ' Saving...' : ''),
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return Observable.merge(challengeCompleted$, updateUser$)
|
||||||
|
.delay(300)
|
||||||
|
.startWith(hikeCompleted(finalAnswer))
|
||||||
|
.catch(error => Observable.just({
|
||||||
|
type: 'error',
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
// end with action so we know it is ok to transition
|
||||||
|
.doOnCompleted(() => dispatch({ type: types.transitionHike }))
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => ({ getState, dispatch }) => next => {
|
||||||
|
return function answerSaga(action) {
|
||||||
|
if (action.type === types.answerQuestion) {
|
||||||
|
return handleAnswer(getState, dispatch, next, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// let goToNextQuestion hit reducers first
|
||||||
|
const result = next(action);
|
||||||
|
if (action.type === types.transitionHike) {
|
||||||
|
const { hikesApp: { currentHike } } = getState();
|
||||||
|
// if no next hike currentHike will equal '' which is falsy
|
||||||
|
if (currentHike) {
|
||||||
|
dispatch(push(`/videos/${currentHike}`));
|
||||||
|
} else {
|
||||||
|
dispatch(push('/map'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
45
common/app/routes/Hikes/redux/fetch-hikes-saga.js
Normal file
45
common/app/routes/Hikes/redux/fetch-hikes-saga.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { normalize, Schema, arrayOf } from 'normalizr';
|
||||||
|
// import debug from 'debug';
|
||||||
|
|
||||||
|
import types from './types';
|
||||||
|
import { fetchHikesCompleted } from './actions';
|
||||||
|
import { handleError } from '../../../redux/types';
|
||||||
|
|
||||||
|
import { findCurrentHike } from './utils';
|
||||||
|
|
||||||
|
// const log = debug('fcc:fetch-hikes-saga');
|
||||||
|
const hike = new Schema('hike', { idAttribute: 'dashedName' });
|
||||||
|
|
||||||
|
export default ({ services }) => ({ dispatch }) => next => {
|
||||||
|
return function fetchHikesSaga(action) {
|
||||||
|
if (action.type !== types.fetchHikes) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashedName = action.payload;
|
||||||
|
return services.readService$({ service: 'hikes' })
|
||||||
|
.map(hikes => {
|
||||||
|
const { entities, result } = normalize(
|
||||||
|
{ hikes },
|
||||||
|
{ hikes: arrayOf(hike) }
|
||||||
|
);
|
||||||
|
|
||||||
|
hikes = {
|
||||||
|
entities: entities.hike,
|
||||||
|
results: result.hikes
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentHike = findCurrentHike(hikes, dashedName);
|
||||||
|
|
||||||
|
return fetchHikesCompleted(hikes, currentHike);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return Observable.just({
|
||||||
|
type: handleError,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
8
common/app/routes/Hikes/redux/index.js
Normal file
8
common/app/routes/Hikes/redux/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export actions from './actions';
|
||||||
|
export reducer from './reducer';
|
||||||
|
export types from './types';
|
||||||
|
|
||||||
|
import answerSaga from './answer-saga';
|
||||||
|
import fetchHikesSaga from './fetch-hikes-saga';
|
||||||
|
|
||||||
|
export const sagas = [ answerSaga, fetchHikesSaga ];
|
@ -3,7 +3,7 @@ import { Observable } from 'rx';
|
|||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:hikes:actions');
|
const debug = debugFactory('fcc:hikes:actions');
|
||||||
const noOp = { transform: () => {} };
|
const noOp = { transform: () => {} };
|
||||||
|
|
||||||
function getCurrentHike(hikes = [{}], dashedName, currentHike) {
|
function getCurrentHike(hikes = [{}], dashedName, currentHike) {
|
99
common/app/routes/Hikes/redux/reducer.js
Normal file
99
common/app/routes/Hikes/redux/reducer.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { handleActions } from 'redux-actions';
|
||||||
|
import types from './types';
|
||||||
|
import { findNextHikeName } from './utils';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
hikes: {
|
||||||
|
results: [],
|
||||||
|
entities: {}
|
||||||
|
},
|
||||||
|
// ui
|
||||||
|
// hike dashedName
|
||||||
|
currentHike: '',
|
||||||
|
// 1 indexed
|
||||||
|
currentQuestion: 1,
|
||||||
|
// [ xPosition, yPosition ]
|
||||||
|
mouse: [ 0, 0 ],
|
||||||
|
// change in mouse position since pressed
|
||||||
|
// [ xDelta, yDelta ]
|
||||||
|
delta: [ 0, 0 ],
|
||||||
|
isPressed: false,
|
||||||
|
isCorrect: false,
|
||||||
|
shouldShakeQuestion: false,
|
||||||
|
shouldShowQuestions: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleActions(
|
||||||
|
{
|
||||||
|
[types.toggleQuestionView]: state => ({
|
||||||
|
...state,
|
||||||
|
shouldShowQuestions: !state.shouldShowQuestions,
|
||||||
|
currentQuestion: 1
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.grabQuestion]: (state, { payload: { delta, mouse } }) => ({
|
||||||
|
...state,
|
||||||
|
isPressed: true,
|
||||||
|
delta,
|
||||||
|
mouse
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.releaseQuestion]: state => ({
|
||||||
|
...state,
|
||||||
|
isPressed: false,
|
||||||
|
mouse: [ 0, 0 ]
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.moveQuestion]: (state, { payload: mouse }) => ({ ...state, mouse }),
|
||||||
|
|
||||||
|
[types.resetHike]: state => ({
|
||||||
|
...state,
|
||||||
|
currentQuestion: 1,
|
||||||
|
shouldShowQuestions: false,
|
||||||
|
mouse: [0, 0],
|
||||||
|
delta: [0, 0]
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.startShake]: state => ({ ...state, shouldShakeQuestion: true }),
|
||||||
|
[types.endShake]: state => ({ ...state, shouldShakeQuestion: false }),
|
||||||
|
|
||||||
|
[types.primeNextQuestion]: (state, { payload: userAnswer }) => ({
|
||||||
|
...state,
|
||||||
|
currentQuestion: state.currentQuestion + 1,
|
||||||
|
mouse: [ userAnswer ? 1000 : -1000, 0],
|
||||||
|
isPressed: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.goToNextQuestion]: state => ({
|
||||||
|
...state,
|
||||||
|
mouse: [ 0, 0 ]
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.hikeCompleted]: (state, { payload: userAnswer } ) => ({
|
||||||
|
...state,
|
||||||
|
isCorrect: true,
|
||||||
|
isPressed: false,
|
||||||
|
delta: [ 0, 0 ],
|
||||||
|
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.goToNextHike]: state => ({
|
||||||
|
...state,
|
||||||
|
currentHike: findNextHikeName(state.hikes, state.currentHike),
|
||||||
|
showQuestions: false,
|
||||||
|
currentQuestion: 1,
|
||||||
|
mouse: [ 0, 0 ]
|
||||||
|
}),
|
||||||
|
|
||||||
|
[types.fetchHikesCompleted]: (state, { payload }) => {
|
||||||
|
const { hikes, currentHike } = payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
hikes,
|
||||||
|
currentHike
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialState
|
||||||
|
);
|
8
common/app/routes/Hikes/redux/selectors.js
Normal file
8
common/app/routes/Hikes/redux/selectors.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// use this file for common selectors
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
export const getCurrentHike = createSelector(
|
||||||
|
state => state.hikesApp.hikes.entities,
|
||||||
|
state => state.hikesApp.currentHike,
|
||||||
|
(hikesMap, currentHikeDashedName) => (hikesMap[currentHikeDashedName] || {})
|
||||||
|
);
|
24
common/app/routes/Hikes/redux/types.js
Normal file
24
common/app/routes/Hikes/redux/types.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import createTypes from '../../../utils/create-types';
|
||||||
|
|
||||||
|
export default createTypes([
|
||||||
|
'fetchHikes',
|
||||||
|
'fetchHikesCompleted',
|
||||||
|
'resetHike',
|
||||||
|
|
||||||
|
'toggleQuestionView',
|
||||||
|
'grabQuestion',
|
||||||
|
'releaseQuestion',
|
||||||
|
'moveQuestion',
|
||||||
|
|
||||||
|
'answerQuestion',
|
||||||
|
|
||||||
|
'startShake',
|
||||||
|
'endShake',
|
||||||
|
|
||||||
|
'primeNextQuestion',
|
||||||
|
'goToNextQuestion',
|
||||||
|
'transitionHike',
|
||||||
|
|
||||||
|
'hikeCompleted',
|
||||||
|
'goToNextHike'
|
||||||
|
], 'videos');
|
77
common/app/routes/Hikes/redux/utils.js
Normal file
77
common/app/routes/Hikes/redux/utils.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const log = debug('fcc:hikes:utils');
|
||||||
|
|
||||||
|
function getFirstHike(hikes) {
|
||||||
|
return hikes.results[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface Hikes {
|
||||||
|
// results: String[],
|
||||||
|
// entities: {
|
||||||
|
// hikeId: Challenge
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// findCurrentHike({
|
||||||
|
// hikes: Hikes,
|
||||||
|
// dashedName: String
|
||||||
|
// }) => String
|
||||||
|
export function findCurrentHike(hikes, dashedName) {
|
||||||
|
if (!dashedName) {
|
||||||
|
return getFirstHike(hikes) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterRegex = new RegExp(dashedName, 'i');
|
||||||
|
|
||||||
|
return hikes
|
||||||
|
.results
|
||||||
|
.filter(dashedName => {
|
||||||
|
return filterRegex.test(dashedName);
|
||||||
|
})
|
||||||
|
.reduce((throwAway, hike) => {
|
||||||
|
return hike;
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentHike(hikes = {}, dashedName) {
|
||||||
|
if (!dashedName) {
|
||||||
|
return getFirstHike(hikes) || {};
|
||||||
|
}
|
||||||
|
return hikes.entities[dashedName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// findNextHikeName(
|
||||||
|
// hikes: { results: String[] },
|
||||||
|
// dashedName: String
|
||||||
|
// ) => String
|
||||||
|
export function findNextHikeName({ results }, dashedName) {
|
||||||
|
if (!dashedName) {
|
||||||
|
log('find next hike no id provided');
|
||||||
|
return results[0];
|
||||||
|
}
|
||||||
|
const currentIndex = _.findIndex(
|
||||||
|
results,
|
||||||
|
_dashedName => _dashedName === dashedName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentIndex >= results.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return results[currentIndex + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getMouse(e, [dx, dy]) {
|
||||||
|
let { pageX, pageY, touches, changedTouches } = e;
|
||||||
|
|
||||||
|
// touches can be empty on touchend
|
||||||
|
if (touches || changedTouches) {
|
||||||
|
e.preventDefault();
|
||||||
|
// these re-assigns the values of pageX, pageY from touches
|
||||||
|
({ pageX, pageY } = touches[0] || changedTouches[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pageX - dx, pageY - dy];
|
||||||
|
}
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
306
common/app/routes/Jobs/components/JobTotal.jsx
Normal file
306
common/app/routes/Jobs/components/JobTotal.jsx
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import { CompositeDisposable } from 'rx';
|
||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Button, Input, Col, Row, Well } from 'react-bootstrap';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { push } from 'react-router-redux';
|
||||||
|
import PureComponent from 'react-pure-render/component';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyPromo,
|
||||||
|
clearPromo,
|
||||||
|
updatePromo
|
||||||
|
} from '../redux/actions';
|
||||||
|
|
||||||
|
// real paypal buttons
|
||||||
|
// will take your money
|
||||||
|
const paypalIds = {
|
||||||
|
regular: 'Q8Z82ZLAX3Q8N',
|
||||||
|
highlighted: 'VC8QPSKCYMZLN'
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindableActions = {
|
||||||
|
applyPromo,
|
||||||
|
clearPromo,
|
||||||
|
push,
|
||||||
|
updatePromo
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
state => state.jobsApp.newJob,
|
||||||
|
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 {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this._subscriptions = new CompositeDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
if (!this.props.id) {
|
||||||
|
this.props.push('/jobs');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.clearPromo();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._subscriptions.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
applyPromo,
|
||||||
|
updatePromo
|
||||||
|
} = 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={ updatePromo }
|
||||||
|
type='text'
|
||||||
|
value={ promoCode } />
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
onClick={ () => {
|
||||||
|
const subscription = applyPromo({
|
||||||
|
id,
|
||||||
|
code: promoCode,
|
||||||
|
type: isHighlighted ? 'isHighlighted' : null
|
||||||
|
}).subscribe();
|
||||||
|
this._subscriptions.add(subscription);
|
||||||
|
}}>
|
||||||
|
Apply Promo Code
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isHighlighted,
|
||||||
|
buttonId,
|
||||||
|
price,
|
||||||
|
discountAmount,
|
||||||
|
push
|
||||||
|
} = 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={ () => setTimeout(push, 0, '/jobs') }
|
||||||
|
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);
|
@ -1,132 +1,150 @@
|
|||||||
import React, { cloneElement, PropTypes } from 'react';
|
import React, { cloneElement, PropTypes } from 'react';
|
||||||
import { contain } from 'thundercats-react';
|
import { compose } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
|
||||||
|
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 mapStateToProps = createSelector(
|
||||||
children: PropTypes.element,
|
state => state.jobsApp.jobs.entities,
|
||||||
appActions: PropTypes.object,
|
state => state.jobsApp.jobs.results,
|
||||||
jobActions: PropTypes.object,
|
state => state.jobsApp,
|
||||||
jobs: PropTypes.array,
|
(jobsMap, jobsById) => {
|
||||||
showModal: PropTypes.bool
|
return { jobs: jobsById.map(id => jobsMap[id]) };
|
||||||
},
|
}
|
||||||
|
);
|
||||||
|
|
||||||
handleJobClick(id) {
|
const bindableActions = {
|
||||||
const { appActions, jobActions } = this.props;
|
findJob,
|
||||||
if (!id) {
|
fetchJobs
|
||||||
return null;
|
};
|
||||||
}
|
|
||||||
jobActions.findJob(id);
|
|
||||||
appActions.goTo(`/jobs/${id}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderList(handleJobClick, jobs) {
|
const fetchOptions = {
|
||||||
return (
|
fetchAction: 'fetchJobs',
|
||||||
<ListJobs
|
isPrimed({ jobs }) {
|
||||||
handleClick={ handleJobClick }
|
return jobs.length > 1;
|
||||||
jobs={ jobs }/>
|
}
|
||||||
);
|
};
|
||||||
},
|
|
||||||
|
|
||||||
renderChild(child, jobs) {
|
export class Jobs extends PureComponent {
|
||||||
if (!child) {
|
static displayName = 'Jobs';
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return cloneElement(
|
|
||||||
child,
|
|
||||||
{ jobs }
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
static propTypes = {
|
||||||
const {
|
push: PropTypes.func,
|
||||||
children,
|
findJob: PropTypes.func,
|
||||||
jobs,
|
fetchJobs: PropTypes.func,
|
||||||
appActions
|
children: PropTypes.element,
|
||||||
} = this.props;
|
jobs: PropTypes.array,
|
||||||
|
showModal: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
createJobClickHandler() {
|
||||||
|
const { findJob } = this.props;
|
||||||
|
|
||||||
|
return (id) => {
|
||||||
|
findJob(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList(handleJobClick, jobs) {
|
||||||
|
return (
|
||||||
|
<ListJobs
|
||||||
|
handleClick={ handleJobClick }
|
||||||
|
jobs={ jobs }/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChild(child, jobs) {
|
||||||
|
if (!child) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cloneElement(
|
||||||
|
child,
|
||||||
|
{ jobs }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
jobs
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset= { 1 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<h1 className='text-center'>
|
||||||
|
Hire a JavaScript engineer who's experienced in HTML5,
|
||||||
|
Node.js, MongoDB, and Agile Development.
|
||||||
|
</h1>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row className='text-center'>
|
||||||
|
<Col
|
||||||
|
sm={ 8 }
|
||||||
|
smOffset={ 2 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<LinkContainer to='/jobs/new' >
|
||||||
|
<Button className='signup-btn btn-block btn-cta'>
|
||||||
|
Post a job: $1,000
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
<div className='spacer' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
md={ 10 }
|
md={ 2 }
|
||||||
mdOffset= { 1 }
|
xs={ 4 }>
|
||||||
xs={ 12 }>
|
<img
|
||||||
<h1 className='text-center'>
|
alt={`
|
||||||
Hire a JavaScript engineer who's experienced in HTML5,
|
a photo of Michael Gai, who recently hired a software
|
||||||
Node.js, MongoDB, and Agile Development.
|
engineer through Free Code Camp
|
||||||
</h1>
|
`}
|
||||||
<div className='spacer' />
|
className='img-responsive testimonial-image-jobs img-center'
|
||||||
<Row className='text-center'>
|
src='http://i.imgur.com/tGcAA8H.jpg' />
|
||||||
<Col
|
</Col>
|
||||||
sm={ 8 }
|
<Col
|
||||||
smOffset={ 2 }
|
md={ 10 }
|
||||||
xs={ 12 }>
|
xs={ 8 }>
|
||||||
<Button
|
<blockquote>
|
||||||
className='signup-btn btn-block btn-cta'
|
<p>
|
||||||
onClick={ ()=> {
|
We hired our last developer out of Free Code Camp
|
||||||
appActions.goTo('/jobs/new');
|
and couldn't be happier. Free Code Camp is now
|
||||||
}}>
|
our go-to way to bring on pre-screened candidates
|
||||||
Post a job: $1,000
|
who are enthusiastic about learning quickly and
|
||||||
</Button>
|
becoming immediately productive in their new career.
|
||||||
<div className='spacer' />
|
</p>
|
||||||
</Col>
|
<footer>
|
||||||
</Row>
|
Michael Gai, <cite>CEO at CoNarrative</cite>
|
||||||
<div className='spacer' />
|
</footer>
|
||||||
<Row>
|
</blockquote>
|
||||||
<Col
|
</Col>
|
||||||
md={ 2 }
|
</Row>
|
||||||
xs={ 4 }>
|
<Row>
|
||||||
<img
|
|
||||||
alt={`
|
|
||||||
a photo of Michael Gai, who recently hired a software
|
|
||||||
engineer through Free Code Camp
|
|
||||||
`}
|
|
||||||
className='img-responsive testimonial-image-jobs img-center'
|
|
||||||
src='http://i.imgur.com/tGcAA8H.jpg' />
|
|
||||||
</Col>
|
|
||||||
<Col
|
|
||||||
md={ 10 }
|
|
||||||
xs={ 8 }>
|
|
||||||
<blockquote>
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<footer>
|
|
||||||
Michael Gai, <cite>CEO at CoNarrative</cite>
|
|
||||||
</footer>
|
|
||||||
</blockquote>
|
|
||||||
</Col>
|
|
||||||
</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(mapStateToProps, bindableActions),
|
||||||
|
contain(fetchOptions)
|
||||||
|
)(Jobs);
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
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,31 +21,35 @@ export default React.createClass({
|
|||||||
{ locale }
|
{ locale }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
renderJobs(handleClick, jobs = []) {
|
renderJobs(handleClick, jobs = []) {
|
||||||
return jobs
|
return jobs
|
||||||
.filter(({ isPaid, isApproved, isFilled }) => {
|
.filter(({ isPaid, isApproved, isFilled }) => {
|
||||||
return isPaid && isApproved && !isFilled;
|
return isPaid && isApproved && !isFilled;
|
||||||
})
|
})
|
||||||
.map(({
|
.map(({
|
||||||
id,
|
id,
|
||||||
company,
|
company,
|
||||||
position,
|
position,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
locale
|
locale
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const className = classnames({
|
const className = classnames({
|
||||||
'jobs-list': true,
|
'jobs-list': true,
|
||||||
'col-xs-12': true,
|
'col-xs-12': true,
|
||||||
'jobs-list-highlight': isHighlighted
|
'jobs-list-highlight': isHighlighted
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const to = `/jobs/${id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkContainer
|
||||||
|
key={ id }
|
||||||
|
to={ to }>
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
className={ className }
|
className={ className }
|
||||||
key={ id }
|
|
||||||
onClick={ () => handleClick(id) }>
|
onClick={ () => handleClick(id) }>
|
||||||
<div>
|
<div>
|
||||||
<h4 className='pull-left' style={{ display: 'inline-block' }}>
|
<h4 className='pull-left' style={{ display: 'inline-block' }}>
|
||||||
@ -60,9 +66,10 @@ export default React.createClass({
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
);
|
</LinkContainer>
|
||||||
});
|
);
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@ -76,4 +83,4 @@ export default React.createClass({
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { helpers } from 'rx';
|
import { helpers } from 'rx';
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { History } from 'react-router';
|
import { push } from 'react-router-redux';
|
||||||
import { contain } from 'thundercats-react';
|
import { reduxForm } from 'redux-form';
|
||||||
import debugFactory from 'debug';
|
// import debug from 'debug';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
import normalizeUrl from 'normalize-url';
|
|
||||||
|
|
||||||
import { getDefaults } from '../utils';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
inHTMLData,
|
isAscii,
|
||||||
uriInSingleQuotedAttr
|
isEmail,
|
||||||
} from 'xss-filters';
|
isURL
|
||||||
|
} from 'validator';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -20,30 +18,14 @@ import {
|
|||||||
Row
|
Row
|
||||||
} from 'react-bootstrap';
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
import {
|
import { saveForm, loadSavedForm } from '../redux/actions';
|
||||||
isAscii,
|
|
||||||
isEmail,
|
|
||||||
isURL
|
|
||||||
} from 'validator';
|
|
||||||
|
|
||||||
const debug = debugFactory('freecc:jobs:newForm');
|
// const log = debug('fcc:jobs:newForm');
|
||||||
|
|
||||||
const checkValidity = [
|
|
||||||
'position',
|
|
||||||
'locale',
|
|
||||||
'description',
|
|
||||||
'email',
|
|
||||||
'url',
|
|
||||||
'logo',
|
|
||||||
'company',
|
|
||||||
'isHighlighted',
|
|
||||||
'howToApply'
|
|
||||||
];
|
|
||||||
const hightlightCopy = `
|
const hightlightCopy = `
|
||||||
Highlight my post to make it stand out. (+$250)
|
Highlight my post to make it stand out. (+$250)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
const isRemoteCopy = `
|
const isRemoteCopy = `
|
||||||
This job can be performed remotely.
|
This job can be performed remotely.
|
||||||
`;
|
`;
|
||||||
@ -60,196 +42,103 @@ const checkboxClass = dedent`
|
|||||||
col-sm-6 col-md-offset-3
|
col-sm-6 col-md-offset-3
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function formatValue(value, validator, type = 'string') {
|
const certTypes = {
|
||||||
const formatted = getDefaults(type);
|
isFrontEndCert: 'isFrontEndCert',
|
||||||
if (validator && type === 'string' && typeof value === 'string') {
|
isBackEndCert: 'isBackEndCert'
|
||||||
formatted.valid = validator(value);
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
formatted.value = value;
|
|
||||||
formatted.bsStyle = formatted.valid ? 'success' : 'error';
|
|
||||||
}
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeOptions = {
|
|
||||||
stripWWW: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatUrl(url, shouldKeepTrailingSlash = true) {
|
|
||||||
if (
|
|
||||||
typeof url === 'string' &&
|
|
||||||
url.length > 4 &&
|
|
||||||
url.indexOf('.') !== -1
|
|
||||||
) {
|
|
||||||
// prevent trailing / from being stripped during typing
|
|
||||||
let lastChar = '';
|
|
||||||
if (shouldKeepTrailingSlash && url.substring(url.length - 1) === '/') {
|
|
||||||
lastChar = '/';
|
|
||||||
}
|
|
||||||
return normalizeUrl(url, normalizeOptions) + lastChar;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidURL(data) {
|
function isValidURL(data) {
|
||||||
return isURL(data, { 'require_protocol': true });
|
return isURL(data, { 'require_protocol': true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
'position',
|
||||||
|
'locale',
|
||||||
|
'description',
|
||||||
|
'email',
|
||||||
|
'url',
|
||||||
|
'logo',
|
||||||
|
'company',
|
||||||
|
'isHighlighted',
|
||||||
|
'isRemoteOk',
|
||||||
|
'isFrontEndCert',
|
||||||
|
'isBackEndCert',
|
||||||
|
'howToApply'
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldValidators = {
|
||||||
|
position: makeRequired(isAscii),
|
||||||
|
locale: makeRequired(isAscii),
|
||||||
|
description: makeRequired(helpers.identity),
|
||||||
|
email: makeRequired(isEmail),
|
||||||
|
url: makeRequired(isValidURL),
|
||||||
|
logo: makeOptional(isValidURL),
|
||||||
|
company: makeRequired(isAscii),
|
||||||
|
howToApply: makeRequired(isAscii)
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeOptional(validator) {
|
||||||
|
return val => val ? validator(val) : true;
|
||||||
|
}
|
||||||
function makeRequired(validator) {
|
function makeRequired(validator) {
|
||||||
return (val) => !!val && validator(val);
|
return (val) => val ? validator(val) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contain({
|
function validateForm(values) {
|
||||||
store: 'appStore',
|
return Object.keys(fieldValidators)
|
||||||
actions: 'jobActions',
|
.map(field => {
|
||||||
map({ jobsApp: { form = {} } }) {
|
if (fieldValidators[field](values[field])) {
|
||||||
const {
|
return null;
|
||||||
position,
|
|
||||||
locale,
|
|
||||||
description,
|
|
||||||
email,
|
|
||||||
url,
|
|
||||||
logo,
|
|
||||||
company,
|
|
||||||
isFrontEndCert = true,
|
|
||||||
isBackEndCert,
|
|
||||||
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: {
|
|
||||||
jobActions: PropTypes.object,
|
|
||||||
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],
|
|
||||||
|
|
||||||
handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const pros = this.props;
|
|
||||||
let valid = true;
|
|
||||||
checkValidity.forEach((prop) => {
|
|
||||||
// if value exist, check if it is valid
|
|
||||||
if (pros[prop].value && pros[prop].type !== 'boolean') {
|
|
||||||
valid = valid && !!pros[prop].valid;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!valid ||
|
|
||||||
!pros.isFrontEndCert &&
|
|
||||||
!pros.isBackEndCert
|
|
||||||
) {
|
|
||||||
debug('form not valid');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return { [field]: !fieldValidators[field](values[field]) };
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((errors, error) => ({ ...errors, ...error }), {});
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
function getBsStyle(field) {
|
||||||
jobActions,
|
if (field.pristine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// form values
|
return field.error ?
|
||||||
position,
|
'error' :
|
||||||
locale,
|
'success';
|
||||||
description,
|
}
|
||||||
email,
|
|
||||||
url,
|
|
||||||
logo,
|
|
||||||
company,
|
|
||||||
isFrontEndCert,
|
|
||||||
isBackEndCert,
|
|
||||||
isHighlighted,
|
|
||||||
isRemoteOk,
|
|
||||||
howToApply
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// sanitize user output
|
export class NewJob extends React.Component {
|
||||||
const jobValues = {
|
static displayName = 'NewJob';
|
||||||
position: inHTMLData(position.value),
|
|
||||||
locale: inHTMLData(locale.value),
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
static propTypes = {
|
||||||
if (jobValues[prop]) {
|
fields: PropTypes.object,
|
||||||
accu[prop] = jobValues[prop];
|
handleSubmit: PropTypes.func,
|
||||||
}
|
loadSavedForm: PropTypes.func,
|
||||||
return accu;
|
push: PropTypes.func,
|
||||||
}, {});
|
saveForm: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
job.postedOn = new Date();
|
componentDidMount() {
|
||||||
debug('job sanitized', job);
|
this.props.loadSavedForm();
|
||||||
jobActions.saveForm(job);
|
}
|
||||||
|
|
||||||
this.history.pushState(null, '/jobs/new/preview');
|
handleSubmit(job) {
|
||||||
},
|
this.props.saveForm(job);
|
||||||
|
this.props.push('/jobs/new/preview');
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
handleCertClick(name) {
|
||||||
const { jobActions } = this.props;
|
const { fields } = this.props;
|
||||||
jobActions.getSavedForm();
|
Object.keys(certTypes).forEach(certType => {
|
||||||
},
|
if (certType === name) {
|
||||||
|
return fields[certType].onChange(true);
|
||||||
|
}
|
||||||
|
return fields[certType].onChange(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleChange(name, { target: { value } }) {
|
render() {
|
||||||
const { jobActions: { handleForm } } = this.props;
|
const {
|
||||||
handleForm({ [name]: value });
|
fields: {
|
||||||
},
|
|
||||||
|
|
||||||
handleCertClick(name) {
|
|
||||||
const { jobActions: { handleForm } } = this.props;
|
|
||||||
const otherButton = name === 'isFrontEndCert' ?
|
|
||||||
'isBackEndCert' :
|
|
||||||
'isFrontEndCert';
|
|
||||||
|
|
||||||
handleForm({
|
|
||||||
[name]: true,
|
|
||||||
[otherButton]: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
position,
|
position,
|
||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
@ -261,235 +150,242 @@ export default contain({
|
|||||||
isRemoteOk,
|
isRemoteOk,
|
||||||
howToApply,
|
howToApply,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isBackEndCert,
|
isBackEndCert
|
||||||
jobActions: { handleForm }
|
},
|
||||||
} = this.props;
|
handleSubmit
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const { handleChange } = this;
|
const { handleChange } = this;
|
||||||
const labelClass = 'col-sm-offset-1 col-sm-2';
|
const labelClass = 'col-sm-offset-1 col-sm-2';
|
||||||
const inputClass = 'col-sm-6';
|
const inputClass = 'col-sm-6';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
md={ 10 }
|
md={ 10 }
|
||||||
mdOffset={ 1 }>
|
mdOffset={ 1 }>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<form
|
<form
|
||||||
className='form-horizontal'
|
className='form-horizontal'
|
||||||
onSubmit={ this.handleSubmit }>
|
onSubmit={ handleSubmit(data => this.handleSubmit(data)) }>
|
||||||
|
|
||||||
<div className='spacer'>
|
<div className='spacer'>
|
||||||
<h2>First, select your ideal applicant: </h2>
|
<h2>First, select your ideal applicant: </h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
xs={ 6 }
|
||||||
|
xsOffset={ 3 }>
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
bsStyle='primary'
|
||||||
|
className={ isFrontEndCert.value ? 'active' : '' }
|
||||||
|
onClick={ () => {
|
||||||
|
if (!isFrontEndCert.value) {
|
||||||
|
this.handleCertClick(certTypes.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.value ? 'active' : ''}
|
||||||
|
onClick={ () => {
|
||||||
|
if (!isBackEndCert.value) {
|
||||||
|
this.handleCertClick(certTypes.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={ getBsStyle(position) }
|
||||||
|
label='Job Title'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
placeholder={
|
||||||
|
'e.g. Full Stack Developer, Front End Developer, etc.'
|
||||||
|
}
|
||||||
|
required={ true }
|
||||||
|
type='text'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...position }
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(locale) }
|
||||||
|
label='Location'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
placeholder='e.g. San Francisco, Remote, etc.'
|
||||||
|
required={ true }
|
||||||
|
type='text'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...locale }
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(description) }
|
||||||
|
label='Description'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
required={ true }
|
||||||
|
rows='10'
|
||||||
|
type='textarea'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...description }
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={ isRemoteCopy }
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={ checkboxClass }
|
||||||
|
{ ...isRemoteOk }
|
||||||
|
/>
|
||||||
|
<div className='spacer' />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<Row>
|
||||||
|
<div>
|
||||||
|
<h2>How should they apply?</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(howToApply) }
|
||||||
|
label=' '
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
placeholder={ howToApplyCopy }
|
||||||
|
required={ true }
|
||||||
|
rows='2'
|
||||||
|
type='textarea'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...howToApply }
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className='spacer' />
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<h2>Tell us about your organization</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(company) }
|
||||||
|
label='Company Name'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('company', e) }
|
||||||
|
type='text'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...company }
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(email) }
|
||||||
|
label='Email'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
placeholder='This is how we will contact you'
|
||||||
|
required={ true }
|
||||||
|
type='email'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...email }
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(url) }
|
||||||
|
label='URL'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
placeholder='http://yourcompany.com'
|
||||||
|
type='url'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...url }
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
bsStyle={ getBsStyle(logo) }
|
||||||
|
label='Logo'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
placeholder='http://yourcompany.com/logo.png'
|
||||||
|
type='url'
|
||||||
|
wrapperClassName={ inputClass }
|
||||||
|
{ ...logo }
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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'
|
||||||
);
|
label={ hightlightCopy }
|
||||||
}
|
type='checkbox'
|
||||||
})
|
wrapperClassName={
|
||||||
);
|
checkboxClass.replace('text-left', '')
|
||||||
|
}
|
||||||
|
{ ...isHighlighted }
|
||||||
|
/>
|
||||||
|
</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(
|
||||||
|
{
|
||||||
|
form: 'NewJob',
|
||||||
|
fields,
|
||||||
|
validate: validateForm
|
||||||
|
},
|
||||||
|
state => ({ initialValues: state.jobsApp.initialValues }),
|
||||||
|
{
|
||||||
|
loadSavedForm,
|
||||||
|
push,
|
||||||
|
saveForm
|
||||||
|
}
|
||||||
|
)(NewJob);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
@ -1,79 +1,94 @@
|
|||||||
|
import { CompositeDisposable } from 'rx';
|
||||||
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 'react-redux';
|
||||||
|
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 { clearForm, saveJob } from '../redux/actions';
|
||||||
{
|
|
||||||
store: 'appStore',
|
const mapStateToProps = state => ({ job: state.jobsApp.newJob });
|
||||||
actions: [
|
|
||||||
'appActions',
|
const bindableActions = {
|
||||||
'jobActions'
|
goBack,
|
||||||
],
|
push,
|
||||||
map({ jobsApp: { form: job = {} } }) {
|
clearForm,
|
||||||
return { job };
|
saveJob
|
||||||
|
};
|
||||||
|
|
||||||
|
export class JobPreview extends PureComponent {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this._subscriptions = new CompositeDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
static displayName = 'Preview';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
job: PropTypes.object,
|
||||||
|
saveJob: PropTypes.func,
|
||||||
|
clearForm: PropTypes.func,
|
||||||
|
push: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const { push, job } = this.props;
|
||||||
|
// redirect user in client
|
||||||
|
if (!job || !job.position || !job.description) {
|
||||||
|
push('/jobs/new');
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
React.createClass({
|
|
||||||
displayName: 'Preview',
|
|
||||||
|
|
||||||
propTypes: {
|
componentWillUnmount() {
|
||||||
appActions: PropTypes.object,
|
this._subscriptions.dispose();
|
||||||
job: PropTypes.object,
|
}
|
||||||
jobActions: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount() {
|
handleJobSubmit() {
|
||||||
const { appActions, job } = this.props;
|
const { clearForm, saveJob, job } = this.props;
|
||||||
// redirect user in client
|
clearForm();
|
||||||
if (!job || !job.position || !job.description) {
|
const subscription = saveJob(job).subscribe();
|
||||||
appActions.goTo('/jobs/new');
|
this._subscriptions.add(subscription);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { appActions, job, jobActions } = this.props;
|
const { job, goBack } = this.props;
|
||||||
|
|
||||||
if (!job || !job.position || !job.description) {
|
if (!job || !job.position || !job.description) {
|
||||||
return <JobNotFound />;
|
return <JobNotFound />;
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ShowJob job={ job } />
|
|
||||||
<div className='spacer'></div>
|
|
||||||
<hr />
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 10 }
|
|
||||||
mdOffset={ 1 }
|
|
||||||
xs={ 12 }>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
className='signup-btn'
|
|
||||||
onClick={ () => {
|
|
||||||
jobActions.clearSavedForm();
|
|
||||||
jobActions.saveJobToDb({
|
|
||||||
goTo: '/jobs/new/check-out',
|
|
||||||
job
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
|
|
||||||
Looks great! Let's Check Out
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
onClick={ () => appActions.goBack() } >
|
|
||||||
Head back and make edits
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
);
|
return (
|
||||||
|
<div>
|
||||||
|
<ShowJob job={ job } />
|
||||||
|
<div className='spacer'></div>
|
||||||
|
<hr />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
className='signup-btn'
|
||||||
|
onClick={ () => this.handleJobSubmit() }>
|
||||||
|
|
||||||
|
Looks great! Let's Check Out
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
onClick={ goBack } >
|
||||||
|
Head back and make edits
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, bindableActions)(JobPreview);
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { History } from 'react-router';
|
import { compose } from 'redux';
|
||||||
import { contain } from 'thundercats-react';
|
import { connect } from 'react-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 { fetchJobs } from '../redux/actions';
|
||||||
|
|
||||||
import ShowJob from './ShowJob.jsx';
|
import ShowJob from './ShowJob.jsx';
|
||||||
import JobNotFound from './JobNotFound.jsx';
|
import JobNotFound from './JobNotFound.jsx';
|
||||||
@ -51,86 +57,90 @@ 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',
|
state => state.jobsApp.jobs.entities,
|
||||||
map({
|
({ username, isFrontEndCert, isBackEndCert }, currentJob, jobs) => ({
|
||||||
username,
|
username,
|
||||||
isFrontEndCert,
|
isFrontEndCert,
|
||||||
isBackEndCert,
|
isBackEndCert,
|
||||||
jobsApp: { currentJob }
|
job: jobs[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 <JobNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSignedIn = !!username;
|
|
||||||
|
|
||||||
const showApply = shouldShowApply(
|
|
||||||
job,
|
|
||||||
{ isFrontEndCert, isBackEndCert }
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = generateMessage(
|
|
||||||
job,
|
|
||||||
{ isFrontEndCert, isBackEndCert, isSignedIn }
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShowJob
|
|
||||||
message={ message }
|
|
||||||
preview={ false }
|
|
||||||
showApply={ showApply }
|
|
||||||
{ ...this.props }/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const bindableActions = {
|
||||||
|
push,
|
||||||
|
fetchJobs
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOptions = {
|
||||||
|
fetchAction: 'fetchJobs',
|
||||||
|
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 <JobNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSignedIn = !!username;
|
||||||
|
|
||||||
|
const showApply = shouldShowApply(
|
||||||
|
job,
|
||||||
|
{ isFrontEndCert, isBackEndCert }
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = generateMessage(
|
||||||
|
job,
|
||||||
|
{ isFrontEndCert, isBackEndCert, isSignedIn }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShowJob
|
||||||
|
message={ message }
|
||||||
|
preview={ false }
|
||||||
|
showApply={ showApply }
|
||||||
|
{ ...this.props }/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps, bindableActions),
|
||||||
|
contain(fetchOptions)
|
||||||
|
)(Show);
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default from './Actions';
|
|
@ -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
|
||||||
|
34
common/app/routes/Jobs/redux/actions.js
Normal file
34
common/app/routes/Jobs/redux/actions.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
// saves to database
|
||||||
|
export const saveJob = createAction(types.saveJob);
|
||||||
|
// saves to localStorage
|
||||||
|
export const saveForm = createAction(types.saveForm);
|
||||||
|
|
||||||
|
export const saveCompleted = createAction(types.saveCompleted);
|
||||||
|
|
||||||
|
export const clearForm = createAction(types.clearForm);
|
||||||
|
|
||||||
|
export const loadSavedForm = createAction(types.loadSavedForm);
|
||||||
|
export const loadSavedFormCompleted = createAction(
|
||||||
|
types.loadSavedFormCompleted
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clearPromo = createAction(types.clearPromo);
|
||||||
|
export const updatePromo = createAction(
|
||||||
|
types.updatePromo,
|
||||||
|
({ target: { value = '' } = {} } = {}) => value
|
||||||
|
);
|
||||||
|
|
||||||
|
export const applyPromo = createAction(types.applyPromo);
|
||||||
|
export const applyPromoCompleted = createAction(types.applyPromoCompleted);
|
39
common/app/routes/Jobs/redux/apply-promo-saga.js
Normal file
39
common/app/routes/Jobs/redux/apply-promo-saga.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
import { applyPromo } from './types';
|
||||||
|
import { applyPromoCompleted } from './actions';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
|
||||||
|
export default () => ({ dispatch }) => next => {
|
||||||
|
return function applyPromoSaga(action) {
|
||||||
|
if (action.type !== applyPromo) {
|
||||||
|
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 applyPromoCompleted(promo);
|
||||||
|
})
|
||||||
|
.catch(error => Observable.just({
|
||||||
|
type: 'app.handleError',
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
50
common/app/routes/Jobs/redux/fetch-jobs-saga.js
Normal file
50
common/app/routes/Jobs/redux/fetch-jobs-saga.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import { normalize, Schema, arrayOf } from 'normalizr';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (action.type !== fetchJobs) {
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { payload: id } = action;
|
||||||
|
const data = { service: 'jobs' };
|
||||||
|
if (id) {
|
||||||
|
data.params = { 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.job,
|
||||||
|
results: result.jobs
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return Observable.just({
|
||||||
|
type: handleError,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
11
common/app/routes/Jobs/redux/index.js
Normal file
11
common/app/routes/Jobs/redux/index.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export actions from './actions';
|
||||||
|
export reducer from './reducer';
|
||||||
|
export types from './types';
|
||||||
|
|
||||||
|
import fetchJobsSaga from './fetch-jobs-saga';
|
||||||
|
import saveJobSaga from './save-job-saga';
|
||||||
|
import applyPromoSaga from './apply-promo-saga';
|
||||||
|
|
||||||
|
export formNormalizer from './jobs-form-normalizer';
|
||||||
|
|
||||||
|
export const sagas = [ fetchJobsSaga, saveJobSaga, applyPromoSaga ];
|
42
common/app/routes/Jobs/redux/jobs-form-normalizer.js
Normal file
42
common/app/routes/Jobs/redux/jobs-form-normalizer.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import normalizeUrl from 'normalize-url';
|
||||||
|
import {
|
||||||
|
inHTMLData,
|
||||||
|
uriInSingleQuotedAttr
|
||||||
|
} from 'xss-filters';
|
||||||
|
|
||||||
|
const normalizeOptions = {
|
||||||
|
stripWWW: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function ifDefinedNormalize(normalizer) {
|
||||||
|
return value => value ? normalizer(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUrl(url) {
|
||||||
|
if (
|
||||||
|
typeof url === 'string' &&
|
||||||
|
url.length > 4 &&
|
||||||
|
url.indexOf('.') !== -1
|
||||||
|
) {
|
||||||
|
// prevent trailing / from being stripped during typing
|
||||||
|
let lastChar = '';
|
||||||
|
if (url.substring(url.length - 1) === '/') {
|
||||||
|
lastChar = '/';
|
||||||
|
}
|
||||||
|
return normalizeUrl(url, normalizeOptions) + lastChar;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
NewJob: {
|
||||||
|
position: ifDefinedNormalize(inHTMLData),
|
||||||
|
locale: ifDefinedNormalize(inHTMLData),
|
||||||
|
description: ifDefinedNormalize(inHTMLData),
|
||||||
|
email: ifDefinedNormalize(inHTMLData),
|
||||||
|
url: ifDefinedNormalize(value => formatUrl(uriInSingleQuotedAttr(value))),
|
||||||
|
logo: ifDefinedNormalize(value => formatUrl(uriInSingleQuotedAttr(value))),
|
||||||
|
company: ifDefinedNormalize(inHTMLData),
|
||||||
|
howToApply: ifDefinedNormalize(inHTMLData)
|
||||||
|
}
|
||||||
|
};
|
85
common/app/routes/Jobs/redux/reducer.js
Normal file
85
common/app/routes/Jobs/redux/reducer.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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 = {
|
||||||
|
// used by NewJob form
|
||||||
|
initialValues: {},
|
||||||
|
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.fetchJobsCompleted]: (state, { payload: { jobs, currentJob } }) => ({
|
||||||
|
...state,
|
||||||
|
currentJob,
|
||||||
|
jobs
|
||||||
|
}),
|
||||||
|
[types.updatePromo]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
promoCode: replace(payload)
|
||||||
|
}),
|
||||||
|
[types.saveCompleted]: (state, { payload: newJob }) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
newJob
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.loadSavedFormCompleted]: (state, { payload: initialValues }) => ({
|
||||||
|
...state,
|
||||||
|
initialValues
|
||||||
|
}),
|
||||||
|
[types.applyPromoCompleted]: (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
|
||||||
|
);
|
32
common/app/routes/Jobs/redux/save-job-saga.js
Normal file
32
common/app/routes/Jobs/redux/save-job-saga.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { push } from 'react-router-redux';
|
||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
import { saveCompleted } from './actions';
|
||||||
|
import { saveJob } from './types';
|
||||||
|
|
||||||
|
import { handleError } from '../../../redux/types';
|
||||||
|
|
||||||
|
export default ({ services }) => ({ dispatch }) => next => {
|
||||||
|
return function saveJobSaga(action) {
|
||||||
|
const result = next(action);
|
||||||
|
if (action.type !== saveJob) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const { payload: job } = action;
|
||||||
|
|
||||||
|
return services.createService$({
|
||||||
|
service: 'jobs',
|
||||||
|
params: { job }
|
||||||
|
})
|
||||||
|
.retry(3)
|
||||||
|
.flatMap(job => Observable.of(
|
||||||
|
saveCompleted(job),
|
||||||
|
push('/jobs/new/check-out')
|
||||||
|
))
|
||||||
|
.catch(error => Observable.just({
|
||||||
|
type: handleError,
|
||||||
|
error
|
||||||
|
}))
|
||||||
|
.doOnNext(dispatch);
|
||||||
|
};
|
||||||
|
};
|
22
common/app/routes/Jobs/redux/types.js
Normal file
22
common/app/routes/Jobs/redux/types.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import createTypes from '../../../utils/create-types';
|
||||||
|
|
||||||
|
export default createTypes([
|
||||||
|
'fetchJobs',
|
||||||
|
'fetchJobsCompleted',
|
||||||
|
|
||||||
|
'findJob',
|
||||||
|
'saveJob',
|
||||||
|
'saveForm',
|
||||||
|
|
||||||
|
'saveCompleted',
|
||||||
|
|
||||||
|
'clearForm',
|
||||||
|
|
||||||
|
'loadSavedForm',
|
||||||
|
'loadSavedFormCompleted',
|
||||||
|
|
||||||
|
'clearPromo',
|
||||||
|
'updatePromo',
|
||||||
|
'applyPromo',
|
||||||
|
'applyPromoCompleted'
|
||||||
|
], 'jobs');
|
9
common/app/sagas.js
Normal file
9
common/app/sagas.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { sagas as appSagas } from './redux';
|
||||||
|
import { sagas as hikesSagas} from './routes/Hikes/redux';
|
||||||
|
import { sagas as jobsSagas } from './routes/Jobs/redux';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...appSagas,
|
||||||
|
...hikesSagas,
|
||||||
|
...jobsSagas
|
||||||
|
];
|
42
common/app/utils/Professor-Context.js
Normal file
42
common/app/utils/Professor-Context.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { Children, PropTypes } from 'react';
|
||||||
|
|
||||||
|
class ProfessorContext extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.professor = props.professor;
|
||||||
|
}
|
||||||
|
static displayName = 'ProfessorContext';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
professor: PropTypes.object,
|
||||||
|
children: PropTypes.element.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
static childContextTypes = {
|
||||||
|
professor: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return { professor: this.professor };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return Children.only(this.props.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable react/display-name, react/prop-types */
|
||||||
|
ProfessorContext.wrap = function wrap(Component, professor) {
|
||||||
|
const props = {};
|
||||||
|
if (professor) {
|
||||||
|
props.professor = professor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
ProfessorContext,
|
||||||
|
props,
|
||||||
|
Component
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfessorContext;
|
10
common/app/utils/create-types.js
Normal file
10
common/app/utils/create-types.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// createTypes(types: String[], prefix: String) => Object
|
||||||
|
export default function createTypes(types = [], prefix = '') {
|
||||||
|
if (!Array.isArray(types) || typeof prefix !== 'string') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return types.reduce((types, type) => {
|
||||||
|
types[type] = prefix + '.' + type;
|
||||||
|
return types;
|
||||||
|
}, {});
|
||||||
|
}
|
192
common/app/utils/professor-x.js
Normal file
192
common/app/utils/professor-x.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import React, { PropTypes, createElement } from 'react';
|
||||||
|
import { Observable, CompositeDisposable } from 'rx';
|
||||||
|
import shouldComponentUpdate from 'react-pure-render/function';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
// interface contain {
|
||||||
|
// (options?: Object, Component: ReactComponent) => ReactComponent
|
||||||
|
// (options?: Object) => (Component: ReactComponent) => ReactComponent
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Action: { type: String, payload: Any, ...meta }
|
||||||
|
//
|
||||||
|
// ActionCreator(...args) => Observable
|
||||||
|
//
|
||||||
|
// interface options {
|
||||||
|
// fetchAction?: ActionCreator,
|
||||||
|
// getActionArgs?(props: Object, context: Object) => [],
|
||||||
|
// isPrimed?(props: Object, context: Object) => Boolean,
|
||||||
|
// handleError?(err) => Void
|
||||||
|
// shouldRefetch?(
|
||||||
|
// props: Object,
|
||||||
|
// nextProps: Object,
|
||||||
|
// context: Object,
|
||||||
|
// nextContext: Object
|
||||||
|
// ) => Boolean,
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
const log = debug('fcc:professerx');
|
||||||
|
|
||||||
|
function getChildContext(childContextTypes, currentContext) {
|
||||||
|
|
||||||
|
const compContext = { ...currentContext };
|
||||||
|
// istanbul ignore else
|
||||||
|
if (!childContextTypes || !childContextTypes.professor) {
|
||||||
|
delete compContext.professor;
|
||||||
|
}
|
||||||
|
return compContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const __DEV__ = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
export default function contain(options = {}, Component) {
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (!Component) {
|
||||||
|
return contain.bind(null, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
let action;
|
||||||
|
let isActionable = false;
|
||||||
|
let hasRefetcher = typeof options.shouldRefetch === 'function';
|
||||||
|
|
||||||
|
const getActionArgs = typeof options.getActionArgs === 'function' ?
|
||||||
|
options.getActionArgs :
|
||||||
|
(() => []);
|
||||||
|
|
||||||
|
const isPrimed = typeof options.isPrimed === 'function' ?
|
||||||
|
options.isPrimed :
|
||||||
|
(() => false);
|
||||||
|
|
||||||
|
|
||||||
|
return class Container extends React.Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.__subscriptions = new CompositeDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
static displayName = `Container(${Component.displayName})`;
|
||||||
|
static contextTypes = {
|
||||||
|
...Component.contextTypes,
|
||||||
|
professor: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const { professor } = this.context;
|
||||||
|
const { props } = this;
|
||||||
|
if (!options.fetchAction) {
|
||||||
|
log(`${Component.displayName} has no fetch action defined`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
action = props[options.fetchAction];
|
||||||
|
isActionable = typeof action === 'function';
|
||||||
|
|
||||||
|
if (__DEV__ && typeof action !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`${options.fetchAction} should return a function but got ${action}.
|
||||||
|
Check the fetch options for ${Component.displayName}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!professor ||
|
||||||
|
!professor.fetchContext
|
||||||
|
) {
|
||||||
|
log(
|
||||||
|
`${Component.displayName} did not have professor defined on context`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const actionArgs = getActionArgs(
|
||||||
|
props,
|
||||||
|
getChildContext(Component.contextTypes, this.context)
|
||||||
|
);
|
||||||
|
|
||||||
|
return professor.fetchContext.push({
|
||||||
|
name: options.fetchAction,
|
||||||
|
action,
|
||||||
|
actionArgs,
|
||||||
|
component: Component.displayName || 'Anon'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (isPrimed(this.props, this.context)) {
|
||||||
|
log('container is primed');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isActionable) {
|
||||||
|
log(`${Component.displayName} container is not actionable`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const actionArgs = getActionArgs(this.props, this.context);
|
||||||
|
const fetch$ = action.apply(null, actionArgs);
|
||||||
|
if (__DEV__ && !Observable.isObservable(fetch$)) {
|
||||||
|
console.log(fetch$);
|
||||||
|
throw new Error(
|
||||||
|
`Action creator should return an Observable but got ${fetch$}.
|
||||||
|
Check the action creator for fetch action ${options.fetchAction}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = fetch$.subscribe(
|
||||||
|
() => {},
|
||||||
|
options.handleError
|
||||||
|
);
|
||||||
|
return this.__subscriptions.add(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps, nextContext) {
|
||||||
|
if (
|
||||||
|
!isActionable ||
|
||||||
|
!hasRefetcher ||
|
||||||
|
!options.shouldRefetch(
|
||||||
|
this.props,
|
||||||
|
nextProps,
|
||||||
|
getChildContext(Component.contextTypes, this.context),
|
||||||
|
getChildContext(Component.contextTypes, nextContext)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionArgs = getActionArgs(
|
||||||
|
this.props,
|
||||||
|
getChildContext(Component.contextTypes, this.context)
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetch$ = action.apply(null, actionArgs);
|
||||||
|
if (__DEV__ && !Observable.isObservable(fetch$)) {
|
||||||
|
throw new Error(
|
||||||
|
'fetch action should return observable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = fetch$.subscribe(
|
||||||
|
() => {},
|
||||||
|
options.errorHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
this.__subscriptions.add(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.__subscriptions) {
|
||||||
|
this.__subscriptions.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate = shouldComponentUpdate;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props } = this;
|
||||||
|
|
||||||
|
return createElement(
|
||||||
|
Component,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
52
common/app/utils/render-to-string.js
Normal file
52
common/app/utils/render-to-string.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Observable, Scheduler } from 'rx';
|
||||||
|
import ReactDOM from 'react-dom/server';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
import ProfessorContext from './Professor-Context';
|
||||||
|
|
||||||
|
const log = debug('fcc:professor');
|
||||||
|
|
||||||
|
export function fetch({ fetchContext = [] }) {
|
||||||
|
if (fetchContext.length === 0) {
|
||||||
|
log('empty fetch context found');
|
||||||
|
return Observable.just(fetchContext);
|
||||||
|
}
|
||||||
|
return Observable.from(fetchContext, null, null, Scheduler.default)
|
||||||
|
.doOnNext(({ name }) => log(`calling ${name} action creator`))
|
||||||
|
.map(({ action, actionArgs }) => action.apply(null, actionArgs))
|
||||||
|
.doOnNext(fetch$ => {
|
||||||
|
if (!Observable.isObservable(fetch$)) {
|
||||||
|
throw new Error(
|
||||||
|
'action creator should return an observable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(fetch$ => fetch$.doOnNext(action => log('action', action.type)))
|
||||||
|
.mergeAll()
|
||||||
|
.doOnCompleted(() => log('all fetch observables completed'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function renderToString(Component) {
|
||||||
|
const fetchContext = [];
|
||||||
|
const professor = { fetchContext };
|
||||||
|
let ContextedComponent;
|
||||||
|
try {
|
||||||
|
ContextedComponent = ProfessorContext.wrap(Component, professor);
|
||||||
|
log('initiating fetcher registration');
|
||||||
|
ReactDOM.renderToStaticMarkup(ContextedComponent);
|
||||||
|
log('fetcher registration completed');
|
||||||
|
} catch (e) {
|
||||||
|
return Observable.throw(e);
|
||||||
|
}
|
||||||
|
return fetch(professor)
|
||||||
|
.last()
|
||||||
|
.delay(0)
|
||||||
|
.map(() => {
|
||||||
|
const markup = ReactDOM.renderToString(Component);
|
||||||
|
return {
|
||||||
|
markup,
|
||||||
|
fetchContext
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
26
common/app/utils/render.js
Normal file
26
common/app/utils/render.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Disposable, Observable } from 'rx';
|
||||||
|
import ProfessorContext from './Professor-Context';
|
||||||
|
|
||||||
|
export default function render(Component, DOMContainer) {
|
||||||
|
let ContextedComponent;
|
||||||
|
try {
|
||||||
|
ContextedComponent = ProfessorContext.wrap(Component);
|
||||||
|
} catch (e) {
|
||||||
|
return Observable.throw(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.create(observer => {
|
||||||
|
try {
|
||||||
|
ReactDOM.render(ContextedComponent, DOMContainer, function() {
|
||||||
|
observer.onNext(this);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return observer.onError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Disposable.create(() => {
|
||||||
|
return ReactDOM.unmountComponentAtNode(DOMContainer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
37
common/app/utils/shallow-equals.js
Normal file
37
common/app/utils/shallow-equals.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// original sourc
|
||||||
|
// https://github.com/rackt/react-redux/blob/master/src/utils/shallowEqual.js
|
||||||
|
// MIT license
|
||||||
|
export default function shallowEqual(objA, objB) {
|
||||||
|
if (objA === objB) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof objA !== 'object' ||
|
||||||
|
objA === null ||
|
||||||
|
typeof objB !== 'object' ||
|
||||||
|
objB === null
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysA = Object.keys(objA);
|
||||||
|
var keysB = Object.keys(objB);
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for A's keys different from B.
|
||||||
|
var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
|
||||||
|
for (var i = 0; i < keysA.length; i++) {
|
||||||
|
if (
|
||||||
|
!bHasOwnProperty(keysA[i]) ||
|
||||||
|
objA[keysA[i]] !== objB[keysA[i]]
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
@ -10,7 +10,7 @@ import {
|
|||||||
|
|
||||||
const { defaultProfileImage } = require('../utils/constantStrings.json');
|
const { defaultProfileImage } = require('../utils/constantStrings.json');
|
||||||
const githubRegex = (/github/i);
|
const githubRegex = (/github/i);
|
||||||
const debug = debugFactory('freecc:models:userIdent');
|
const debug = debugFactory('fcc:models:userIdent');
|
||||||
|
|
||||||
function createAccessToken(user, ttl, cb) {
|
function createAccessToken(user, ttl, cb) {
|
||||||
if (arguments.length === 2 && typeof ttl === 'function') {
|
if (arguments.length === 2 && typeof ttl === 'function') {
|
||||||
@ -73,7 +73,7 @@ export default function(UserIdent) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
cb(err, user, identity);
|
return cb(err, user, identity);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -99,12 +99,12 @@ export default function(UserIdent) {
|
|||||||
} else {
|
} else {
|
||||||
query = { username: userObj.username };
|
query = { username: userObj.username };
|
||||||
}
|
}
|
||||||
userModel.findOrCreate({ where: query }, userObj, function(err, user) {
|
return userModel.findOrCreate({ where: query }, userObj, (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
var date = new Date();
|
var date = new Date();
|
||||||
userIdentityModel.create({
|
return userIdentityModel.create({
|
||||||
provider: getSocialProvider(provider),
|
provider: getSocialProvider(provider),
|
||||||
externalId: profile.id,
|
externalId: profile.id,
|
||||||
authScheme: authScheme,
|
authScheme: authScheme,
|
||||||
@ -122,7 +122,7 @@ export default function(UserIdent) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
cb(err, user, identity);
|
return cb(err, user, identity);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -134,7 +134,7 @@ export default function(UserIdent) {
|
|||||||
debug('no user identity instance found');
|
debug('no user identity instance found');
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
userIdent.user(function(err, user) {
|
return userIdent.user(function(err, user) {
|
||||||
let userChanged = false;
|
let userChanged = false;
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -175,11 +175,11 @@ export default function(UserIdent) {
|
|||||||
if (userChanged) {
|
if (userChanged) {
|
||||||
return user.save(function(err) {
|
return user.save(function(err) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
next();
|
return next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
debug('exiting after user identity before save');
|
debug('exiting after user identity before save');
|
||||||
next();
|
return next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { isAlphanumeric, isHexadecimal } from 'validator';
|
import { isAlphanumeric, isHexadecimal } from 'validator';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
const log = debug('freecc:models:promo');
|
const log = debug('fcc:models:promo');
|
||||||
|
|
||||||
export default function promo(Promo) {
|
export default function promo(Promo) {
|
||||||
Promo.getButton = function getButton(id, code, type = 'isNot') {
|
Promo.getButton = function getButton(id, code, type = 'isNot') {
|
||||||
|
@ -7,7 +7,7 @@ import debugFactory from 'debug';
|
|||||||
import { saveUser, observeMethod } from '../../server/utils/rx';
|
import { saveUser, observeMethod } from '../../server/utils/rx';
|
||||||
import { blacklistedUsernames } from '../../server/utils/constants';
|
import { blacklistedUsernames } from '../../server/utils/constants';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:user:remote');
|
const debug = debugFactory('fcc:user:remote');
|
||||||
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
||||||
|
|
||||||
function getAboutProfile({
|
function getAboutProfile({
|
||||||
@ -271,7 +271,7 @@ module.exports = function(User) {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
User.findOne({ where: { username } }, (err, user) => {
|
return User.findOne({ where: { username } }, (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
@ -327,7 +327,7 @@ module.exports = function(User) {
|
|||||||
.valueOf();
|
.valueOf();
|
||||||
const user$ = findUser({ where: { username: receiver }});
|
const user$ = findUser({ where: { username: receiver }});
|
||||||
|
|
||||||
user$
|
return user$
|
||||||
.tapOnNext((user) => {
|
.tapOnNext((user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`could not find receiver for ${ receiver }`);
|
throw new Error(`could not find receiver for ${ receiver }`);
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { Observable, AnonymousObservable, helpers } from 'rx';
|
import { Observable, AnonymousObservable, helpers } from 'rx';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:ajax$');
|
const debug = debugFactory('fcc:ajax$');
|
||||||
const root = typeof window !== 'undefined' ? window : {};
|
const root = typeof window !== 'undefined' ? window : {};
|
||||||
|
|
||||||
// Gets the proper XMLHttpRequest for support for older IE
|
// Gets the proper XMLHttpRequest for support for older IE
|
||||||
|
48
common/utils/services-creator.js
Normal file
48
common/utils/services-creator.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Observable, Disposable } from 'rx';
|
||||||
|
import Fetchr from 'fetchr';
|
||||||
|
import stampit from 'stampit';
|
||||||
|
|
||||||
|
function callbackObserver(observer) {
|
||||||
|
return (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return observer.onError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.onNext(res);
|
||||||
|
return observer.onCompleted();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default stampit({
|
||||||
|
init({ args: [ options ] }) {
|
||||||
|
this.services = new Fetchr(options);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
readService$({ service: resource, params, config }) {
|
||||||
|
return Observable.create(observer => {
|
||||||
|
this.services.read(
|
||||||
|
resource,
|
||||||
|
params,
|
||||||
|
config,
|
||||||
|
callbackObserver(observer)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Disposable.create(() => observer.dispose());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createService$({ service: resource, params, body, config }) {
|
||||||
|
return Observable.create(observer => {
|
||||||
|
this.services.create(
|
||||||
|
resource,
|
||||||
|
params,
|
||||||
|
body,
|
||||||
|
config,
|
||||||
|
callbackObserver(observer)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Disposable.create(() => observer.dispose());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
11
gulpfile.js
11
gulpfile.js
@ -1,5 +1,5 @@
|
|||||||
// enable debug for gulp
|
// enable debug for gulp
|
||||||
process.env.DEBUG = process.env.DEBUG || 'freecc:*';
|
process.env.DEBUG = process.env.DEBUG || 'fcc:*';
|
||||||
|
|
||||||
require('babel-core/register');
|
require('babel-core/register');
|
||||||
var Rx = require('rx'),
|
var Rx = require('rx'),
|
||||||
@ -12,7 +12,7 @@ var Rx = require('rx'),
|
|||||||
gutil = require('gulp-util'),
|
gutil = require('gulp-util'),
|
||||||
reduce = require('gulp-reduce-file'),
|
reduce = require('gulp-reduce-file'),
|
||||||
sortKeys = require('sort-keys'),
|
sortKeys = require('sort-keys'),
|
||||||
debug = require('debug')('freecc:gulp'),
|
debug = require('debug')('fcc:gulp'),
|
||||||
yargs = require('yargs'),
|
yargs = require('yargs'),
|
||||||
concat = require('gulp-concat'),
|
concat = require('gulp-concat'),
|
||||||
uglify = require('gulp-uglify'),
|
uglify = require('gulp-uglify'),
|
||||||
@ -98,7 +98,10 @@ var paths = {
|
|||||||
'public/bower_components/bootstrap/dist/js/bootstrap.min.js',
|
'public/bower_components/bootstrap/dist/js/bootstrap.min.js',
|
||||||
'public/bower_components/d3/d3.min.js',
|
'public/bower_components/d3/d3.min.js',
|
||||||
'public/bower_components/moment/min/moment.min.js',
|
'public/bower_components/moment/min/moment.min.js',
|
||||||
'public/bower_components/moment-timezone/builds/moment-timezone-with-data.min.js',
|
|
||||||
|
'public/bower_components/' +
|
||||||
|
'moment-timezone/builds/moment-timezone-with-data.min.js',
|
||||||
|
|
||||||
'public/bower_components/mousetrap/mousetrap.min.js',
|
'public/bower_components/mousetrap/mousetrap.min.js',
|
||||||
'public/bower_components/lightbox2/dist/js/lightbox.min.js',
|
'public/bower_components/lightbox2/dist/js/lightbox.min.js',
|
||||||
'public/bower_components/rxjs/dist/rx.all.min.js'
|
'public/bower_components/rxjs/dist/rx.all.min.js'
|
||||||
@ -194,7 +197,7 @@ gulp.task('serve', ['build-manifest'], function(cb) {
|
|||||||
exec: path.join(__dirname, 'node_modules/.bin/babel-node'),
|
exec: path.join(__dirname, 'node_modules/.bin/babel-node'),
|
||||||
env: {
|
env: {
|
||||||
'NODE_ENV': process.env.NODE_ENV || 'development',
|
'NODE_ENV': process.env.NODE_ENV || 'development',
|
||||||
'DEBUG': process.env.DEBUG || 'freecc:*'
|
'DEBUG': process.env.DEBUG || 'fcc:*'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('start', function() {
|
.on('start', function() {
|
||||||
|
18
package.json
18
package.json
@ -49,7 +49,7 @@
|
|||||||
"emmet-codemirror": "^1.2.5",
|
"emmet-codemirror": "^1.2.5",
|
||||||
"errorhandler": "^1.4.2",
|
"errorhandler": "^1.4.2",
|
||||||
"es6-map": "~0.1.1",
|
"es6-map": "~0.1.1",
|
||||||
"eslint": "~1.10.2",
|
"eslint": "^2.2.0",
|
||||||
"eslint-plugin-react": "^4.1.0",
|
"eslint-plugin-react": "^4.1.0",
|
||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
"express-flash": "~0.0.2",
|
"express-flash": "~0.0.2",
|
||||||
@ -61,7 +61,7 @@
|
|||||||
"gulp": "^3.9.0",
|
"gulp": "^3.9.0",
|
||||||
"gulp-babel": "^6.1.1",
|
"gulp-babel": "^6.1.1",
|
||||||
"gulp-concat": "^2.6.0",
|
"gulp-concat": "^2.6.0",
|
||||||
"gulp-eslint": "^1.1.0",
|
"gulp-eslint": "^2.0.0",
|
||||||
"gulp-jsonlint": "^1.1.0",
|
"gulp-jsonlint": "^1.1.0",
|
||||||
"gulp-less": "^3.0.3",
|
"gulp-less": "^3.0.3",
|
||||||
"gulp-nodemon": "^2.0.3",
|
"gulp-nodemon": "^2.0.3",
|
||||||
@ -74,7 +74,7 @@
|
|||||||
"gulp-util": "^3.0.6",
|
"gulp-util": "^3.0.6",
|
||||||
"helmet": "^1.1.0",
|
"helmet": "^1.1.0",
|
||||||
"helmet-csp": "^1.0.3",
|
"helmet-csp": "^1.0.3",
|
||||||
"history": "^1.17.0",
|
"history": "^2.0.0",
|
||||||
"jade": "^1.11.0",
|
"jade": "^1.11.0",
|
||||||
"json-loader": "~0.5.2",
|
"json-loader": "~0.5.2",
|
||||||
"less": "^2.5.1",
|
"less": "^2.5.1",
|
||||||
@ -92,6 +92,7 @@
|
|||||||
"node-uuid": "^1.4.3",
|
"node-uuid": "^1.4.3",
|
||||||
"nodemailer": "^2.1.0",
|
"nodemailer": "^2.1.0",
|
||||||
"normalize-url": "^1.3.1",
|
"normalize-url": "^1.3.1",
|
||||||
|
"normalizr": "^2.0.0",
|
||||||
"object.assign": "^4.0.3",
|
"object.assign": "^4.0.3",
|
||||||
"passport-facebook": "^2.0.0",
|
"passport-facebook": "^2.0.0",
|
||||||
"passport-github": "^1.0.0",
|
"passport-github": "^1.0.0",
|
||||||
@ -105,11 +106,18 @@
|
|||||||
"react-bootstrap": "~0.28.1",
|
"react-bootstrap": "~0.28.1",
|
||||||
"react-dom": "~0.14.3",
|
"react-dom": "~0.14.3",
|
||||||
"react-motion": "~0.4.2",
|
"react-motion": "~0.4.2",
|
||||||
"react-router": "^1.0.0",
|
"react-pure-render": "^1.0.2",
|
||||||
"react-router-bootstrap": "https://github.com/FreeCodeCamp/react-router-bootstrap.git#freecodecamp",
|
"react-redux": "^4.0.6",
|
||||||
|
"react-router": "^2.0.0",
|
||||||
|
"react-router-bootstrap": "~0.20.1",
|
||||||
"react-toastr": "^2.4.0",
|
"react-toastr": "^2.4.0",
|
||||||
|
"react-router-redux": "^2.1.0",
|
||||||
"react-vimeo": "~0.1.0",
|
"react-vimeo": "~0.1.0",
|
||||||
|
"redux": "^3.0.5",
|
||||||
|
"redux-actions": "^0.9.1",
|
||||||
|
"redux-form": "^4.1.4",
|
||||||
"request": "^2.65.0",
|
"request": "^2.65.0",
|
||||||
|
"reselect": "^2.0.2",
|
||||||
"rev-del": "^1.0.5",
|
"rev-del": "^1.0.5",
|
||||||
"rx": "^4.0.0",
|
"rx": "^4.0.0",
|
||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:user:remote');
|
const debug = debugFactory('fcc:user:remote');
|
||||||
|
|
||||||
function destroyAllRelated(id, Model) {
|
function destroyAllRelated(id, Model) {
|
||||||
return Observable.fromNodeCallback(
|
return Observable.fromNodeCallback(
|
||||||
@ -21,7 +21,7 @@ module.exports = function(app) {
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
destroyAllRelated(id, UserIdentity),
|
destroyAllRelated(id, UserIdentity),
|
||||||
destroyAllRelated(id, UserCredential),
|
destroyAllRelated(id, UserCredential),
|
||||||
function(identData, credData) {
|
function(identData, credData) {
|
||||||
@ -30,19 +30,20 @@ module.exports = function(app) {
|
|||||||
credData: credData
|
credData: credData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
).subscribe(
|
)
|
||||||
function(data) {
|
.subscribe(
|
||||||
debug('deleted', data);
|
function(data) {
|
||||||
},
|
debug('deleted', data);
|
||||||
function(err) {
|
},
|
||||||
debug('error deleting user %s stuff', id, err);
|
function(err) {
|
||||||
next(err);
|
debug('error deleting user %s stuff', id, err);
|
||||||
},
|
next(err);
|
||||||
function() {
|
},
|
||||||
debug('user stuff deleted for user %s', id);
|
function() {
|
||||||
next();
|
debug('user stuff deleted for user %s', id);
|
||||||
}
|
next();
|
||||||
);
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// set email varified false on user email signup
|
// set email varified false on user email signup
|
||||||
@ -82,15 +83,15 @@ module.exports = function(app) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
debug('sending welcome email');
|
debug('sending welcome email');
|
||||||
Email.send(mailOptions, function(err) {
|
return Email.send(mailOptions, function(err) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
req.logIn(user, function(err) {
|
return req.logIn(user, function(err) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
|
|
||||||
req.flash('success', {
|
req.flash('success', {
|
||||||
msg: [ "Welcome to Free Code Camp! We've created your account." ]
|
msg: [ "Welcome to Free Code Camp! We've created your account." ]
|
||||||
});
|
});
|
||||||
res.redirect(redirect);
|
return res.redirect(redirect);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import{ Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RoutingContext } from 'react-router';
|
import { RouterContext } from 'react-router';
|
||||||
import Fetchr from 'fetchr';
|
import debug from 'debug';
|
||||||
import { createLocation } from 'history';
|
|
||||||
import debugFactory from 'debug';
|
import renderToString from '../../common/app/utils/render-to-string';
|
||||||
import { dehydrate } from 'thundercats';
|
import provideStore from '../../common/app/provide-store';
|
||||||
import { renderToString$ } from 'thundercats-react';
|
|
||||||
|
|
||||||
import app$ from '../../common/app';
|
import app$ from '../../common/app';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:react-server');
|
const log = debug('fcc:react-server');
|
||||||
|
|
||||||
// add routes here as they slowly get reactified
|
// add routes here as they slowly get reactified
|
||||||
// remove their individual controllers
|
// remove their individual controllers
|
||||||
@ -38,52 +37,43 @@ export default function reactSubRouter(app) {
|
|||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
function serveReactApp(req, res, next) {
|
function serveReactApp(req, res, next) {
|
||||||
const services = new Fetchr({ req });
|
const serviceOptions = { req };
|
||||||
const location = createLocation(req.path);
|
app$({
|
||||||
|
location: req.path,
|
||||||
// returns a router wrapped app
|
serviceOptions
|
||||||
app$({ location })
|
})
|
||||||
// if react-router does not find a route send down the chain
|
// if react-router does not find a route send down the chain
|
||||||
.filter(function({ props }) {
|
.filter(({ redirect, props }) => {
|
||||||
|
if (!props && redirect) {
|
||||||
|
res.redirect(redirect.pathname + redirect.search);
|
||||||
|
}
|
||||||
if (!props) {
|
if (!props) {
|
||||||
debug('react tried to find %s but got 404', location.pathname);
|
log(`react tried to find ${location.pathname} but got 404`);
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
return !!props;
|
return !!props;
|
||||||
})
|
})
|
||||||
.flatMap(function({ props, AppCat }) {
|
.flatMap(({ props, store }) => {
|
||||||
const cat = AppCat(null, services);
|
log('render react markup and pre-fetch data');
|
||||||
debug('render react markup and pre-fetch data');
|
|
||||||
const store = cat.getStore('appStore');
|
|
||||||
|
|
||||||
// primes store to observe action changes
|
return renderToString(
|
||||||
// cleaned up by cat.dispose further down
|
provideStore(React.createElement(RouterContext, props), store)
|
||||||
store.subscribe(() => {});
|
|
||||||
|
|
||||||
return renderToString$(
|
|
||||||
cat,
|
|
||||||
React.createElement(RoutingContext, props)
|
|
||||||
)
|
)
|
||||||
.flatMap(
|
.map(({ markup }) => ({ markup, store }));
|
||||||
dehydrate(cat),
|
|
||||||
({ markup }, data) => ({ markup, data, cat })
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.flatMap(function({ data, markup, cat }) {
|
.flatMap(function({ markup, store }) {
|
||||||
debug('react markup rendered, data fetched');
|
log('react markup rendered, data fetched');
|
||||||
cat.dispose();
|
const state = store.getState();
|
||||||
const { title } = data.AppStore;
|
const { title } = state.app.title;
|
||||||
res.expose(data, 'data');
|
res.expose(state, 'data');
|
||||||
return res.render$(
|
return res.render$(
|
||||||
'layout-react',
|
'layout-react',
|
||||||
{ markup, title }
|
{ markup, title }
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
.doOnNext(markup => res.send(markup))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
function(markup) {
|
() => log('html rendered and ready to send'),
|
||||||
debug('html rendered and ready to send');
|
|
||||||
res.send(markup);
|
|
||||||
},
|
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
|
|
||||||
import certTypes from '../utils/certTypes.json';
|
import certTypes from '../utils/certTypes.json';
|
||||||
|
|
||||||
const log = debug('freecc:certification');
|
const log = debug('fcc:certification');
|
||||||
const sendMessageToNonUser = ifNoUserSend(
|
const sendMessageToNonUser = ifNoUserSend(
|
||||||
'must be logged in to complete.'
|
'must be logged in to complete.'
|
||||||
);
|
);
|
||||||
|
@ -26,7 +26,7 @@ import badIdMap from '../utils/bad-id-map';
|
|||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
const isBeta = !!process.env.BETA;
|
const isBeta = !!process.env.BETA;
|
||||||
const log = debug('freecc:challenges');
|
const log = debug('fcc:challenges');
|
||||||
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
|
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
|
||||||
const challengeView = {
|
const challengeView = {
|
||||||
0: 'challenges/showHTML',
|
0: 'challenges/showHTML',
|
||||||
@ -120,6 +120,7 @@ function shouldShowNew(element, block) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
return newCount / block.length * 100 === 100;
|
return newCount / block.length * 100 === 100;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// meant to be used with a filter method
|
// meant to be used with a filter method
|
||||||
@ -516,7 +517,7 @@ module.exports = function(app) {
|
|||||||
if (data.id) {
|
if (data.id) {
|
||||||
res.cookie('currentChallengeId', data.id);
|
res.cookie('currentChallengeId', data.id);
|
||||||
}
|
}
|
||||||
res.render(view, data);
|
return res.render(view, data);
|
||||||
},
|
},
|
||||||
next,
|
next,
|
||||||
function() {}
|
function() {}
|
||||||
@ -525,14 +526,12 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
function completedChallenge(req, res, next) {
|
function completedChallenge(req, res, next) {
|
||||||
req.checkBody('id', 'id must be a ObjectId').isMongoId();
|
req.checkBody('id', 'id must be a ObjectId').isMongoId();
|
||||||
|
|
||||||
req.checkBody('name', 'name must be at least 3 characters')
|
req.checkBody('name', 'name must be at least 3 characters')
|
||||||
.isString()
|
.isString()
|
||||||
.isLength({ min: 3 });
|
.isLength({ min: 3 });
|
||||||
|
|
||||||
req.checkBody('challengeType', 'challengeType must be an integer')
|
req.checkBody('challengeType', 'challengeType must be an integer')
|
||||||
.isNumber()
|
.isNumber();
|
||||||
.isInt();
|
|
||||||
const type = accepts(req).type('html', 'json', 'text');
|
const type = accepts(req).type('html', 'json', 'text');
|
||||||
|
|
||||||
const errors = req.validationErrors(true);
|
const errors = req.validationErrors(true);
|
||||||
@ -585,7 +584,7 @@ module.exports = function(app) {
|
|||||||
alreadyCompleted
|
alreadyCompleted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -597,8 +596,7 @@ module.exports = function(app) {
|
|||||||
.isString()
|
.isString()
|
||||||
.isLength({ min: 3 });
|
.isLength({ min: 3 });
|
||||||
req.checkBody('challengeType', 'must be a number')
|
req.checkBody('challengeType', 'must be a number')
|
||||||
.isNumber()
|
.isNumber();
|
||||||
.isInt();
|
|
||||||
req.checkBody('solution', 'solution must be a url').isURL();
|
req.checkBody('solution', 'solution must be a url').isURL();
|
||||||
|
|
||||||
const errors = req.validationErrors(true);
|
const errors = req.validationErrors(true);
|
||||||
@ -652,7 +650,7 @@ module.exports = function(app) {
|
|||||||
user.progressTimestamps.length + 1
|
user.progressTimestamps.length + 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.status(200).send(true);
|
return res.status(200).send(true);
|
||||||
})
|
})
|
||||||
.subscribe(() => {}, next);
|
.subscribe(() => {}, next);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ const sendNonUserToCommit = ifNoUserRedirectTo(
|
|||||||
'info'
|
'info'
|
||||||
);
|
);
|
||||||
|
|
||||||
const debug = debugFactory('freecc:commit');
|
const debug = debugFactory('fcc:commit');
|
||||||
|
|
||||||
function findNonprofit(name) {
|
function findNonprofit(name) {
|
||||||
let nonprofit;
|
let nonprofit;
|
||||||
@ -217,7 +217,7 @@ export default function commit(app) {
|
|||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
pledge => {
|
pledge => {
|
||||||
let msg = `You have successfully stopped your pledge.`;
|
let msg = 'You have successfully stopped your pledge.';
|
||||||
if (!pledge) {
|
if (!pledge) {
|
||||||
msg = `No pledge found for user ${user.username}.`;
|
msg = `No pledge found for user ${user.username}.`;
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@ module.exports = function(app) {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
req.user.picture = defaultProfileImage;
|
req.user.picture = defaultProfileImage;
|
||||||
req.user.save(function(err) {
|
return req.user.save(function(err) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
next();
|
return next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +24,6 @@ module.exports = function(app) {
|
|||||||
if (req.user) {
|
if (req.user) {
|
||||||
return res.redirect('/challenges/current-challenge');
|
return res.redirect('/challenges/current-challenge');
|
||||||
}
|
}
|
||||||
res.render('home', { title: message });
|
return res.render('home', { title: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ var Rx = require('rx'),
|
|||||||
async = require('async'),
|
async = require('async'),
|
||||||
moment = require('moment'),
|
moment = require('moment'),
|
||||||
request = require('request'),
|
request = require('request'),
|
||||||
debug = require('debug')('freecc:cntr:resources'),
|
debug = require('debug')('fcc:cntr:resources'),
|
||||||
constantStrings = require('../utils/constantStrings.json'),
|
constantStrings = require('../utils/constantStrings.json'),
|
||||||
labs = require('../resources/labs.json'),
|
labs = require('../resources/labs.json'),
|
||||||
testimonials = require('../resources/testimonials.json'),
|
testimonials = require('../resources/testimonials.json'),
|
||||||
@ -145,7 +145,7 @@ module.exports = function(app) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
process.nextTick(function() {
|
return process.nextTick(function() {
|
||||||
res.header('Content-Type', 'application/xml');
|
res.header('Content-Type', 'application/xml');
|
||||||
res.render('resources/sitemap', {
|
res.render('resources/sitemap', {
|
||||||
appUrl: appUrl,
|
appUrl: appUrl,
|
||||||
@ -227,14 +227,18 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmStickers(req, res) {
|
function confirmStickers(req, res) {
|
||||||
req.flash('success', { msg: 'Thank you for supporting our community! You should receive your stickers in the ' +
|
req.flash('success', {
|
||||||
'mail soon!'});
|
msg: 'Thank you for supporting our community! You should receive ' +
|
||||||
res.redirect('/shop');
|
'your stickers in the mail soon!'
|
||||||
|
});
|
||||||
|
res.redirect('/shop');
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelStickers(req, res) {
|
function cancelStickers(req, res) {
|
||||||
req.flash('info', { msg: 'You\'ve cancelled your purchase of our stickers. You can '
|
req.flash('info', {
|
||||||
+ 'support our community any time by buying some.'});
|
msg: 'You\'ve cancelled your purchase of our stickers. You can ' +
|
||||||
|
'support our community any time by buying some.'
|
||||||
|
});
|
||||||
res.redirect('/shop');
|
res.redirect('/shop');
|
||||||
}
|
}
|
||||||
function submitCatPhoto(req, res) {
|
function submitCatPhoto(req, res) {
|
||||||
@ -280,18 +284,14 @@ module.exports = function(app) {
|
|||||||
function unsubscribe(req, res, next) {
|
function unsubscribe(req, res, next) {
|
||||||
User.findOne({ where: { email: req.params.email } }, function(err, user) {
|
User.findOne({ where: { email: req.params.email } }, function(err, user) {
|
||||||
if (user) {
|
if (user) {
|
||||||
if (err) {
|
if (err) { return next(err); }
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
user.sendMonthlyEmail = false;
|
user.sendMonthlyEmail = false;
|
||||||
user.save(function() {
|
return user.save(function() {
|
||||||
if (err) {
|
if (err) { return next(err); }
|
||||||
return next(err);
|
return res.redirect('/unsubscribed');
|
||||||
}
|
|
||||||
res.redirect('/unsubscribed');
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.redirect('/unsubscribed');
|
return res.redirect('/unsubscribed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -330,7 +330,7 @@ module.exports = function(app) {
|
|||||||
Object.keys(JSON.parse(pulls)).length :
|
Object.keys(JSON.parse(pulls)).length :
|
||||||
'Can\'t connect to github';
|
'Can\'t connect to github';
|
||||||
|
|
||||||
request(
|
return request(
|
||||||
[
|
[
|
||||||
'https://api.github.com/repos/freecodecamp/',
|
'https://api.github.com/repos/freecodecamp/',
|
||||||
'freecodecamp/issues?client_id=',
|
'freecodecamp/issues?client_id=',
|
||||||
@ -344,7 +344,7 @@ module.exports = function(app) {
|
|||||||
issues = ((pulls === parseInt(pulls, 10)) && issues) ?
|
issues = ((pulls === parseInt(pulls, 10)) && issues) ?
|
||||||
Object.keys(JSON.parse(issues)).length - pulls :
|
Object.keys(JSON.parse(issues)).length - pulls :
|
||||||
"Can't connect to GitHub";
|
"Can't connect to GitHub";
|
||||||
res.send({
|
return res.send({
|
||||||
issues: issues,
|
issues: issues,
|
||||||
pulls: pulls
|
pulls: pulls
|
||||||
});
|
});
|
||||||
@ -364,7 +364,7 @@ module.exports = function(app) {
|
|||||||
(JSON.parse(trello)) :
|
(JSON.parse(trello)) :
|
||||||
'Can\'t connect to to Trello';
|
'Can\'t connect to to Trello';
|
||||||
|
|
||||||
res.end(JSON.stringify(trello));
|
return res.end(JSON.stringify(trello));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,7 +379,7 @@ module.exports = function(app) {
|
|||||||
blog = (status && status.statusCode === 200) ?
|
blog = (status && status.statusCode === 200) ?
|
||||||
JSON.parse(blog) :
|
JSON.parse(blog) :
|
||||||
'Can\'t connect to Blogger';
|
'Can\'t connect to Blogger';
|
||||||
res.end(JSON.stringify(blog));
|
return res.end(JSON.stringify(blog));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ var Rx = require('rx'),
|
|||||||
assign = require('object.assign'),
|
assign = require('object.assign'),
|
||||||
sanitizeHtml = require('sanitize-html'),
|
sanitizeHtml = require('sanitize-html'),
|
||||||
moment = require('moment'),
|
moment = require('moment'),
|
||||||
debug = require('debug')('freecc:cntr:story'),
|
debug = require('debug')('fcc:cntr:story'),
|
||||||
utils = require('../utils'),
|
utils = require('../utils'),
|
||||||
observeMethod = require('../utils/rx').observeMethod,
|
observeMethod = require('../utils/rx').observeMethod,
|
||||||
saveUser = require('../utils/rx').saveUser,
|
saveUser = require('../utils/rx').saveUser,
|
||||||
@ -207,7 +207,7 @@ module.exports = function(app) {
|
|||||||
return upvote.upVotedByUsername === username;
|
return upvote.upVotedByUsername === username;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.render('stories/index', {
|
return res.render('stories/index', {
|
||||||
title: story.headline,
|
title: story.headline,
|
||||||
link: story.link,
|
link: story.link,
|
||||||
originalStoryLink: dashedName,
|
originalStoryLink: dashedName,
|
||||||
@ -357,7 +357,7 @@ module.exports = function(app) {
|
|||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
findStory({ where: { link: url } })
|
return findStory({ where: { link: url } })
|
||||||
.map(function(stories) {
|
.map(function(stories) {
|
||||||
if (stories.length) {
|
if (stories.length) {
|
||||||
return {
|
return {
|
||||||
|
@ -19,7 +19,7 @@ import {
|
|||||||
calcLongestStreak
|
calcLongestStreak
|
||||||
} from '../utils/user-stats';
|
} from '../utils/user-stats';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:boot:user');
|
const debug = debugFactory('fcc:boot:user');
|
||||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||||
const certIds = {
|
const certIds = {
|
||||||
[certTypes.frontEnd]: frontEndChallengeId,
|
[certTypes.frontEnd]: frontEndChallengeId,
|
||||||
@ -195,7 +195,7 @@ module.exports = function(app) {
|
|||||||
if (req.user) {
|
if (req.user) {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
res.render('account/signin', {
|
return res.render('account/signin', {
|
||||||
title: 'Sign in to Free Code Camp using a Social Media Account'
|
title: 'Sign in to Free Code Camp using a Social Media Account'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -209,7 +209,7 @@ module.exports = function(app) {
|
|||||||
if (req.user) {
|
if (req.user) {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
res.render('account/email-signin', {
|
return res.render('account/email-signin', {
|
||||||
title: 'Sign in to Free Code Camp using your Email Address'
|
title: 'Sign in to Free Code Camp using your Email Address'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -218,7 +218,7 @@ module.exports = function(app) {
|
|||||||
if (req.user) {
|
if (req.user) {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
res.render('account/email-signup', {
|
return res.render('account/email-signup', {
|
||||||
title: 'Sign up for Free Code Camp using your Email Address'
|
title: 'Sign up for Free Code Camp using your Email Address'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -387,7 +387,7 @@ module.exports = function(app) {
|
|||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: `Looks like user ${username} is not ${certText[certType]}`
|
msg: `Looks like user ${username} is not ${certText[certType]}`
|
||||||
});
|
});
|
||||||
res.redirect('back');
|
return res.redirect('back');
|
||||||
},
|
},
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
@ -406,7 +406,7 @@ module.exports = function(app) {
|
|||||||
section at the bottom of this page.
|
section at the bottom of this page.
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
res.redirect('/' + req.user.username);
|
return res.redirect('/' + req.user.username);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
req.user.isLocked = true;
|
req.user.isLocked = true;
|
||||||
@ -420,7 +420,7 @@ module.exports = function(app) {
|
|||||||
section at the bottom of this page.
|
section at the bottom of this page.
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
res.redirect('/' + req.user.username);
|
return res.redirect('/' + req.user.username);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -429,7 +429,7 @@ module.exports = function(app) {
|
|||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
req.logout();
|
req.logout();
|
||||||
req.flash('info', { msg: 'Your account has been deleted.' });
|
req.flash('info', { msg: 'Your account has been deleted.' });
|
||||||
res.redirect('/');
|
return res.redirect('/');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,7 +438,7 @@ module.exports = function(app) {
|
|||||||
req.flash('errors', { msg: 'access token invalid' });
|
req.flash('errors', { msg: 'access token invalid' });
|
||||||
return res.render('account/forgot');
|
return res.render('account/forgot');
|
||||||
}
|
}
|
||||||
res.render('account/reset', {
|
return res.render('account/reset', {
|
||||||
title: 'Reset your Password',
|
title: 'Reset your Password',
|
||||||
accessToken: req.accessToken.id
|
accessToken: req.accessToken.id
|
||||||
});
|
});
|
||||||
@ -453,14 +453,14 @@ module.exports = function(app) {
|
|||||||
return res.redirect('back');
|
return res.redirect('back');
|
||||||
}
|
}
|
||||||
|
|
||||||
User.findById(req.accessToken.userId, function(err, user) {
|
return User.findById(req.accessToken.userId, function(err, user) {
|
||||||
if (err) { return next(err); }
|
|
||||||
user.updateAttribute('password', password, function(err) {
|
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
|
return user.updateAttribute('password', password, function(err) {
|
||||||
|
if (err) { return next(err); }
|
||||||
|
|
||||||
debug('password reset processed successfully');
|
debug('password reset processed successfully');
|
||||||
req.flash('info', { msg: 'password reset processed successfully' });
|
req.flash('info', { msg: 'password reset processed successfully' });
|
||||||
res.redirect('/');
|
return res.redirect('/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -469,7 +469,7 @@ module.exports = function(app) {
|
|||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
res.render('account/forgot', {
|
return res.render('account/forgot', {
|
||||||
title: 'Forgot Password'
|
title: 'Forgot Password'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -483,7 +483,7 @@ module.exports = function(app) {
|
|||||||
return res.redirect('/forgot');
|
return res.redirect('/forgot');
|
||||||
}
|
}
|
||||||
|
|
||||||
User.resetPassword({
|
return User.resetPassword({
|
||||||
email: email
|
email: email
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -496,7 +496,7 @@ module.exports = function(app) {
|
|||||||
email +
|
email +
|
||||||
' with further instructions.'
|
' with further instructions.'
|
||||||
});
|
});
|
||||||
res.render('account/forgot');
|
return res.render('account/forgot');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,7 +507,7 @@ module.exports = function(app) {
|
|||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
|
|
||||||
req.flash('success', { msg: 'Thanks for voting!' });
|
req.flash('success', { msg: 'Thanks for voting!' });
|
||||||
res.redirect('/map');
|
return res.redirect('/map');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
req.flash('error', { msg: 'You must be signed in to vote.' });
|
req.flash('error', { msg: 'You must be signed in to vote.' });
|
||||||
@ -522,7 +522,7 @@ module.exports = function(app) {
|
|||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
|
|
||||||
req.flash('success', { msg: 'Thanks for voting!' });
|
req.flash('success', { msg: 'Thanks for voting!' });
|
||||||
res.redirect('/map');
|
return res.redirect('/map');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
req.flash('error', {msg: 'You must be signed in to vote.'});
|
req.flash('error', {msg: 'You must be signed in to vote.'});
|
||||||
|
@ -36,8 +36,9 @@ export default function addReturnToUrl() {
|
|||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
req.session.returnTo = req.originalUrl === '/map-aside'
|
req.session.returnTo = req.originalUrl === '/map-aside' ?
|
||||||
? '/map' : req.originalUrl;
|
'/map' :
|
||||||
next();
|
req.originalUrl;
|
||||||
|
return next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,6 @@ export default function({ globalPrepend = '' } = {}) {
|
|||||||
// in production we take use the initially loaded manifest
|
// in production we take use the initially loaded manifest
|
||||||
// since this should not change in production
|
// since this should not change in production
|
||||||
res.locals.rev = boundRev;
|
res.locals.rev = boundRev;
|
||||||
next();
|
return next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import assign from 'object.assign';
|
import assign from 'object.assign';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:services:hikes');
|
const debug = debugFactory('fcc:services:hikes');
|
||||||
|
|
||||||
export default function hikesService(app) {
|
export default function hikesService(app) {
|
||||||
const Challenge = app.models.Challenge;
|
const Challenge = app.models.Challenge;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'hikes',
|
name: 'hikes',
|
||||||
read: (req, resource, params, config, cb) => {
|
read: (req, resource, { dashedName } = {}, config, cb) => {
|
||||||
const query = {
|
const query = {
|
||||||
where: { challengeType: '6' },
|
where: { challengeType: '6' },
|
||||||
order: ['order ASC', 'suborder ASC' ]
|
order: ['order ASC', 'suborder ASC' ]
|
||||||
};
|
};
|
||||||
|
|
||||||
debug('params', params);
|
debug('dashedName', dashedName);
|
||||||
if (params) {
|
if (dashedName) {
|
||||||
assign(query.where, {
|
assign(query.where, {
|
||||||
dashedName: { like: params.dashedName, options: 'i' }
|
dashedName: { like: dashedName, options: 'i' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
debug('query', query);
|
debug('query', query);
|
||||||
@ -25,7 +25,7 @@ export default function hikesService(app) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
cb(null, hikes);
|
return cb(null, hikes.map(hike => hike.toJSON()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -22,18 +22,20 @@ export default function getJobServices(app) {
|
|||||||
isApproved: false
|
isApproved: false
|
||||||
});
|
});
|
||||||
|
|
||||||
Job.create(job, (err, savedJob) => {
|
return Job.create(job, (err, savedJob) => {
|
||||||
cb(err, savedJob);
|
cb(err, savedJob.toJSON());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
read(req, resource, params, config, cb) {
|
read(req, resource, params, config, cb) {
|
||||||
const id = params ? params.id : null;
|
const id = params ? params.id : null;
|
||||||
if (id) {
|
if (id) {
|
||||||
return Job.findById(id, cb);
|
return Job.findById(id)
|
||||||
|
.then(job => cb(null, job.toJSON()))
|
||||||
|
.catch(cb);
|
||||||
}
|
}
|
||||||
Job.find(whereFilt, (err, jobs) => {
|
return Job.find(whereFilt)
|
||||||
cb(err, jobs);
|
.then(jobs => cb(null, jobs.map(job => job.toJSON())))
|
||||||
});
|
.catch(cb);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import debugFactory from 'debug';
|
|||||||
import assign from 'object.assign';
|
import assign from 'object.assign';
|
||||||
|
|
||||||
const censor = '**********************:P********';
|
const censor = '**********************:P********';
|
||||||
const debug = debugFactory('freecc:services:user');
|
const debug = debugFactory('fcc:services:user');
|
||||||
const protectedUserFields = {
|
const protectedUserFields = {
|
||||||
id: censor,
|
id: censor,
|
||||||
password: censor,
|
password: censor,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user