Move Video challenges under challenges dir
Remove old hikes components Remove unused jobs stuff
This commit is contained in:
@ -1,6 +1,5 @@
|
|||||||
import errSaga from './err-saga';
|
import errSaga from './err-saga';
|
||||||
import titleSaga from './title-saga';
|
import titleSaga from './title-saga';
|
||||||
import localStorageSaga from './local-storage-saga';
|
|
||||||
import hardGoToSaga from './hard-go-to-saga';
|
import hardGoToSaga from './hard-go-to-saga';
|
||||||
import windowSaga from './window-saga';
|
import windowSaga from './window-saga';
|
||||||
import executeChallengeSaga from './execute-challenge-saga';
|
import executeChallengeSaga from './execute-challenge-saga';
|
||||||
@ -11,7 +10,6 @@ import gitterSaga from './gitter-saga';
|
|||||||
export default [
|
export default [
|
||||||
errSaga,
|
errSaga,
|
||||||
titleSaga,
|
titleSaga,
|
||||||
localStorageSaga,
|
|
||||||
hardGoToSaga,
|
hardGoToSaga,
|
||||||
windowSaga,
|
windowSaga,
|
||||||
executeChallengeSaga,
|
executeChallengeSaga,
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import store from 'store';
|
|
||||||
import {
|
|
||||||
saveForm,
|
|
||||||
clearForm,
|
|
||||||
loadSavedForm
|
|
||||||
} from '../../common/app/routes/Jobs/redux/types';
|
|
||||||
|
|
||||||
import {
|
|
||||||
saveCompleted,
|
|
||||||
loadSavedFormCompleted
|
|
||||||
} from '../../common/app/routes/Jobs/redux/actions';
|
|
||||||
|
|
||||||
const formKey = 'newJob';
|
|
||||||
|
|
||||||
export default function localStorageSaga(action$) {
|
|
||||||
return action$
|
|
||||||
.filter(action => {
|
|
||||||
return action.type === saveForm ||
|
|
||||||
action.type === clearForm ||
|
|
||||||
action.type === loadSavedForm;
|
|
||||||
})
|
|
||||||
.map(action => {
|
|
||||||
if (action.type === saveForm) {
|
|
||||||
const form = action.payload;
|
|
||||||
try {
|
|
||||||
store.setItem(formKey, form);
|
|
||||||
return saveCompleted(form);
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
type: 'app.handleError',
|
|
||||||
error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.type === clearForm) {
|
|
||||||
store.removeItem(formKey);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadSavedFormCompleted(store.getItem(formKey));
|
|
||||||
});
|
|
||||||
}
|
|
@ -3,27 +3,17 @@ import { reducer as formReducer } from 'redux-form';
|
|||||||
|
|
||||||
import { reducer as app } from './redux';
|
import { reducer as app } from './redux';
|
||||||
import entitiesReducer from './redux/entities-reducer';
|
import entitiesReducer from './redux/entities-reducer';
|
||||||
import { reducer as hikesApp } from './routes/Hikes/redux';
|
|
||||||
import {
|
import {
|
||||||
reducer as challengesApp,
|
reducer as challengesApp,
|
||||||
projectNormalizer
|
projectNormalizer
|
||||||
} from './routes/challenges/redux';
|
} from './routes/challenges/redux';
|
||||||
import {
|
|
||||||
reducer as jobsApp,
|
|
||||||
formNormalizer as jobsNormalizer
|
|
||||||
} from './routes/Jobs/redux';
|
|
||||||
|
|
||||||
export default function createReducer(sideReducers = {}) {
|
export default function createReducer(sideReducers = {}) {
|
||||||
return combineReducers({
|
return combineReducers({
|
||||||
...sideReducers,
|
...sideReducers,
|
||||||
entities: entitiesReducer,
|
entities: entitiesReducer,
|
||||||
app,
|
app,
|
||||||
hikesApp,
|
|
||||||
jobsApp,
|
|
||||||
challengesApp,
|
challengesApp,
|
||||||
form: formReducer.normalize({
|
form: formReducer.normalize({ ...projectNormalizer })
|
||||||
...jobsNormalizer,
|
|
||||||
...projectNormalizer
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { compose } from 'redux';
|
|
||||||
import { contain } from 'redux-epic';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
// import debug from 'debug';
|
|
||||||
|
|
||||||
import HikesMap from './Map.jsx';
|
|
||||||
import { fetchHikes } from '../redux/actions';
|
|
||||||
|
|
||||||
|
|
||||||
// const log = debug('fcc:hikes');
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
state => state.entities.hike,
|
|
||||||
state => state.hikesApp.hikes,
|
|
||||||
(hikesMap, hikesByDashedName) => {
|
|
||||||
if (!hikesMap || !hikesByDashedName) {
|
|
||||||
return { hikes: [] };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
hikes: hikesByDashedName.map(dashedName => hikesMap[dashedName])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
renderMap(hikes) {
|
|
||||||
return (
|
|
||||||
<HikesMap hikes={ hikes }/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { hikes } = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
// render sub-route
|
|
||||||
this.props.children ||
|
|
||||||
// if no sub-route render hikes map
|
|
||||||
this.renderMap(hikes)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// export redux and fetch aware component
|
|
||||||
export default compose(
|
|
||||||
connect(mapStateToProps, { fetchHikes }),
|
|
||||||
contain(fetchOptions)
|
|
||||||
)(Hikes);
|
|
@ -1,102 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Button, Col, Row } from 'react-bootstrap';
|
|
||||||
import Youtube from 'react-youtube';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import debug from 'debug';
|
|
||||||
|
|
||||||
import { hardGoTo } from '../../../redux/actions';
|
|
||||||
import { toggleQuestionView } from '../redux/actions';
|
|
||||||
import { getCurrentHike } from '../redux/selectors';
|
|
||||||
|
|
||||||
const log = debug('fcc:hikes');
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
getCurrentHike,
|
|
||||||
(currentHike) => {
|
|
||||||
const {
|
|
||||||
dashedName,
|
|
||||||
description,
|
|
||||||
challengeSeed: [id] = [0]
|
|
||||||
} = currentHike;
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
dashedName,
|
|
||||||
description
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export class Lecture extends React.Component {
|
|
||||||
static displayName = 'Lecture';
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
// actions
|
|
||||||
toggleQuestionView: PropTypes.func,
|
|
||||||
// ui
|
|
||||||
id: PropTypes.string,
|
|
||||||
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'
|
|
||||||
dangerouslySetInnerHTML={{__html: line}}
|
|
||||||
key={ dashedName + index } />
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id = '1',
|
|
||||||
description = [],
|
|
||||||
toggleQuestionView
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const dashedName = 'foo';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<Row>
|
|
||||||
<Youtube
|
|
||||||
id='player_1'
|
|
||||||
onError={ this.handleError }
|
|
||||||
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);
|
|
@ -1,39 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
displayName: 'HikesMap',
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
hikes: PropTypes.array
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
hikes = [{}]
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const vidElements = hikes.map(({ title, dashedName }) => {
|
|
||||||
return (
|
|
||||||
<ListGroupItem key={ dashedName }>
|
|
||||||
<Link to={ `/videos/${dashedName}` }>
|
|
||||||
<h3>{ title }</h3>
|
|
||||||
</Link>
|
|
||||||
</ListGroupItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className='text-center'>
|
|
||||||
<h2>Welcome To Hikes!</h2>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<ListGroup>
|
|
||||||
{ vidElements }
|
|
||||||
</ListGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,24 +0,0 @@
|
|||||||
export default {
|
|
||||||
path: 'videos',
|
|
||||||
getComponent(_, cb) {
|
|
||||||
require.ensure(
|
|
||||||
[ './components/Hikes.jsx' ],
|
|
||||||
require => {
|
|
||||||
cb(null, require('./components/Hikes.jsx').default);
|
|
||||||
},
|
|
||||||
'hikes'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getChildRoutes(_, cb) {
|
|
||||||
require.ensure(
|
|
||||||
[ './components/Hike.jsx' ],
|
|
||||||
require => {
|
|
||||||
cb(null, [{
|
|
||||||
path: ':dashedName',
|
|
||||||
component: require('./components/Hike.jsx').default
|
|
||||||
}]);
|
|
||||||
},
|
|
||||||
'hikes'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,58 +0,0 @@
|
|||||||
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,
|
|
||||||
(entities, hikes, currentHike) => ({ hikes, currentHike }),
|
|
||||||
entities => ({ entities })
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
@ -1,138 +0,0 @@
|
|||||||
import { Observable } from 'rx';
|
|
||||||
import { push } from 'react-router-redux';
|
|
||||||
|
|
||||||
import types from './types';
|
|
||||||
import { getMouse } from './utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
createErrorObservable,
|
|
||||||
makeToast,
|
|
||||||
updatePoints
|
|
||||||
} from '../../../redux/actions';
|
|
||||||
import { hikeCompleted, goToNextHike } from './actions';
|
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
|
||||||
import { getCurrentHike } from './selectors';
|
|
||||||
|
|
||||||
function handleAnswer(action, getState) {
|
|
||||||
const {
|
|
||||||
e,
|
|
||||||
answer,
|
|
||||||
userAnswer,
|
|
||||||
info,
|
|
||||||
threshold
|
|
||||||
} = action.payload;
|
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
const { id, name, challengeType, tests } = getCurrentHike(state);
|
|
||||||
const {
|
|
||||||
app: { isSignedIn, csrfToken },
|
|
||||||
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 Observable.just(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionX >= threshold) {
|
|
||||||
finalAnswer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionX <= -threshold) {
|
|
||||||
finalAnswer = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finalAnswer = userAnswer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// incorrect question
|
|
||||||
if (answer !== finalAnswer) {
|
|
||||||
let infoAction;
|
|
||||||
if (info) {
|
|
||||||
infoAction = makeToast({
|
|
||||||
title: 'Hint',
|
|
||||||
message: info,
|
|
||||||
type: 'info'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Observable
|
|
||||||
.just({ type: types.endShake })
|
|
||||||
.delay(500)
|
|
||||||
.startWith(infoAction, { type: types.startShake });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tests[currentQuestion]) {
|
|
||||||
return Observable
|
|
||||||
.just({ type: types.goToNextQuestion })
|
|
||||||
.delay(300)
|
|
||||||
.startWith({ type: types.primeNextQuestion });
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateUser$;
|
|
||||||
if (isSignedIn) {
|
|
||||||
const body = { id, name, challengeType: +challengeType, _csrf: csrfToken };
|
|
||||||
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(createErrorObservable);
|
|
||||||
} 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(createErrorObservable)
|
|
||||||
// end with action so we know it is ok to transition
|
|
||||||
.concat(Observable.just({ type: types.transitionHike }));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function answerSaga(action$, getState) {
|
|
||||||
return action$
|
|
||||||
.filter(action => {
|
|
||||||
return action.type === types.answerQuestion ||
|
|
||||||
action.type === types.transitionHike;
|
|
||||||
})
|
|
||||||
.flatMap(action => {
|
|
||||||
if (action.type === types.answerQuestion) {
|
|
||||||
return handleAnswer(action, getState);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hikesApp: { currentHike } } = getState();
|
|
||||||
// if no next hike currentHike will equal '' which is falsy
|
|
||||||
if (currentHike) {
|
|
||||||
return Observable.just(push(`/videos/${currentHike}`));
|
|
||||||
}
|
|
||||||
return Observable.just(push('/map'));
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import { normalize, Schema, arrayOf } from 'normalizr';
|
|
||||||
|
|
||||||
import types from './types';
|
|
||||||
import { fetchHikesCompleted } from './actions';
|
|
||||||
import { createErrorObserable } from '../../../redux/actions';
|
|
||||||
|
|
||||||
import { findCurrentHike } from './utils';
|
|
||||||
|
|
||||||
// const log = debug('fcc:fetch-hikes-saga');
|
|
||||||
const hike = new Schema('hike', { idAttribute: 'dashedName' });
|
|
||||||
|
|
||||||
export default function fetchHikesSaga(action$, getState, { services }) {
|
|
||||||
return action$
|
|
||||||
.filter(action => action.type === types.fetchHikes)
|
|
||||||
.flatMap(action => {
|
|
||||||
const dashedName = action.payload;
|
|
||||||
return services.readService$({ service: 'hikes' })
|
|
||||||
.map(hikes => {
|
|
||||||
const { entities, result } = normalize(
|
|
||||||
{ hikes },
|
|
||||||
{ hikes: arrayOf(hike) }
|
|
||||||
);
|
|
||||||
const currentHike = findCurrentHike(result.hikes, dashedName);
|
|
||||||
return fetchHikesCompleted(entities, result.hikes, currentHike);
|
|
||||||
})
|
|
||||||
.catch(createErrorObserable);
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
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 ];
|
|
@ -1,99 +0,0 @@
|
|||||||
import { handleActions } from 'redux-actions';
|
|
||||||
import types from './types';
|
|
||||||
import { findNextHikeName } from './utils';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
hikes: [],
|
|
||||||
// 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),
|
|
||||||
mouse: [ 0, 0 ]
|
|
||||||
}),
|
|
||||||
|
|
||||||
[types.transitionHike]: state => ({
|
|
||||||
...state,
|
|
||||||
showQuestions: false,
|
|
||||||
currentQuestion: 1
|
|
||||||
}),
|
|
||||||
|
|
||||||
[types.fetchHikesCompleted]: (state, { payload }) => {
|
|
||||||
const { hikes, currentHike } = payload;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
hikes,
|
|
||||||
currentHike
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialState
|
|
||||||
);
|
|
@ -1,8 +0,0 @@
|
|||||||
// use this file for common selectors
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
export const getCurrentHike = createSelector(
|
|
||||||
state => state.entities.hike,
|
|
||||||
state => state.hikesApp.currentHike,
|
|
||||||
(hikesMap, currentHikeDashedName) => (hikesMap[currentHikeDashedName] || {})
|
|
||||||
);
|
|
@ -1,24 +0,0 @@
|
|||||||
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');
|
|
@ -1,76 +0,0 @@
|
|||||||
import debug from 'debug';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const log = debug('fcc:hikes:utils');
|
|
||||||
|
|
||||||
function getFirstHike(hikes) {
|
|
||||||
return hikes[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
|
|
||||||
.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: String[],
|
|
||||||
// dashedName: String
|
|
||||||
// ) => String
|
|
||||||
export function findNextHikeName(hikes, dashedName) {
|
|
||||||
if (!dashedName) {
|
|
||||||
log('find next hike no dashedName provided');
|
|
||||||
return hikes[0];
|
|
||||||
}
|
|
||||||
const currentIndex = _.findIndex(
|
|
||||||
hikes,
|
|
||||||
_dashedName => _dashedName === dashedName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentIndex >= hikes.length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return hikes[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 +0,0 @@
|
|||||||
This folder contains everything relative to Jobs board
|
|
@ -1,35 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { LinkContainer } from 'react-router-bootstrap';
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
|
||||||
|
|
||||||
export default class extends React.Component {
|
|
||||||
static displayName = 'NoJobFound';
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }>
|
|
||||||
<div>
|
|
||||||
No job found...
|
|
||||||
<LinkContainer to='/jobs'>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='primary'>
|
|
||||||
Go to the job board
|
|
||||||
</Button>
|
|
||||||
</LinkContainer>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,306 +0,0 @@
|
|||||||
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='//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,149 +0,0 @@
|
|||||||
import React, { cloneElement, PropTypes } from 'react';
|
|
||||||
import { compose } from 'redux';
|
|
||||||
import { contain } from 'redux-epic';
|
|
||||||
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 ListJobs from './List.jsx';
|
|
||||||
|
|
||||||
import {
|
|
||||||
findJob,
|
|
||||||
fetchJobs
|
|
||||||
} from '../redux/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
state => state.entities.job,
|
|
||||||
state => state.jobsApp.jobs,
|
|
||||||
(jobsMap, jobsById) => ({
|
|
||||||
jobs: jobsById.map(id => jobsMap[id])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const bindableActions = {
|
|
||||||
findJob,
|
|
||||||
fetchJobs
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchOptions = {
|
|
||||||
fetchAction: 'fetchJobs',
|
|
||||||
isPrimed({ jobs }) {
|
|
||||||
return jobs.length > 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Jobs extends PureComponent {
|
|
||||||
static displayName = 'Jobs';
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
push: PropTypes.func,
|
|
||||||
findJob: PropTypes.func,
|
|
||||||
fetchJobs: PropTypes.func,
|
|
||||||
children: PropTypes.element,
|
|
||||||
jobs: PropTypes.array,
|
|
||||||
showModal: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
<Col
|
|
||||||
md={ 2 }
|
|
||||||
xs={ 4 }>
|
|
||||||
<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='//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.renderList(this.createJobClickHandler(), jobs) }
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
connect(mapStateToProps, bindableActions),
|
|
||||||
contain(fetchOptions)
|
|
||||||
)(Jobs);
|
|
@ -1,86 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { LinkContainer } from 'react-router-bootstrap';
|
|
||||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
|
|
||||||
export default class ListJobs extends PureComponent {
|
|
||||||
static displayName = 'ListJobs';
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
handleClick: PropTypes.func,
|
|
||||||
jobs: PropTypes.array
|
|
||||||
};
|
|
||||||
|
|
||||||
addLocation(locale) {
|
|
||||||
if (!locale) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span className='hidden-xs hidden-sm'>
|
|
||||||
{ locale }
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderJobs(handleClick, jobs = []) {
|
|
||||||
return jobs
|
|
||||||
.filter(({ isPaid, isApproved, isFilled }) => {
|
|
||||||
return isPaid && isApproved && !isFilled;
|
|
||||||
})
|
|
||||||
.map(({
|
|
||||||
id,
|
|
||||||
company,
|
|
||||||
position,
|
|
||||||
isHighlighted,
|
|
||||||
locale
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const className = classnames({
|
|
||||||
'jobs-list': true,
|
|
||||||
'col-xs-12': true,
|
|
||||||
'jobs-list-highlight': isHighlighted
|
|
||||||
});
|
|
||||||
|
|
||||||
const to = `/jobs/${id}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LinkContainer
|
|
||||||
key={ id }
|
|
||||||
to={ to }>
|
|
||||||
<ListGroupItem
|
|
||||||
className={ className }
|
|
||||||
onClick={ () => handleClick(id) }>
|
|
||||||
<div>
|
|
||||||
<h4 className='pull-left' style={{ display: 'inline-block' }}>
|
|
||||||
<bold>{ company }</bold>
|
|
||||||
{' '}
|
|
||||||
<span className='hidden-xs hidden-sm'>
|
|
||||||
- { position }
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
<h4
|
|
||||||
className='pull-right'
|
|
||||||
style={{ display: 'inline-block' }}>
|
|
||||||
{ this.addLocation(locale) }
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</ListGroupItem>
|
|
||||||
</LinkContainer>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
handleClick,
|
|
||||||
jobs
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListGroup>
|
|
||||||
{ this.renderJobs(handleClick, jobs) }
|
|
||||||
</ListGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,361 +0,0 @@
|
|||||||
import { helpers } from 'rx';
|
|
||||||
import React, { PropTypes } from 'react';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import { push } from 'react-router-redux';
|
|
||||||
import { reduxForm } from 'redux-form';
|
|
||||||
// import debug from 'debug';
|
|
||||||
import dedent from 'dedent';
|
|
||||||
import { isAscii, isEmail } from 'validator';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Col,
|
|
||||||
Input,
|
|
||||||
Row
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
|
|
||||||
import {
|
|
||||||
isValidURL,
|
|
||||||
makeOptional,
|
|
||||||
makeRequired,
|
|
||||||
createFormValidator,
|
|
||||||
getValidationState
|
|
||||||
} from '../../../utils/form';
|
|
||||||
import { saveForm, loadSavedForm } from '../redux/actions';
|
|
||||||
|
|
||||||
// const log = debug('fcc:jobs:newForm');
|
|
||||||
|
|
||||||
const hightlightCopy = `
|
|
||||||
Highlight my post to make it stand out. (+$250)
|
|
||||||
`;
|
|
||||||
|
|
||||||
const isRemoteCopy = `
|
|
||||||
This job can be performed remotely.
|
|
||||||
`;
|
|
||||||
|
|
||||||
const howToApplyCopy = dedent`
|
|
||||||
Examples: click here to apply yourcompany.com/jobs/33
|
|
||||||
Or email jobs@yourcompany.com
|
|
||||||
`;
|
|
||||||
|
|
||||||
const checkboxClass = dedent`
|
|
||||||
text-left
|
|
||||||
jobs-checkbox-spacer
|
|
||||||
col-sm-offset-2
|
|
||||||
col-sm-6 col-md-offset-3
|
|
||||||
`;
|
|
||||||
|
|
||||||
const certTypes = {
|
|
||||||
isFrontEndCert: 'isFrontEndCert',
|
|
||||||
isBackEndCert: 'isBackEndCert'
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NewJob extends PureComponent {
|
|
||||||
static displayName = 'NewJob';
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
fields: PropTypes.object,
|
|
||||||
handleSubmit: PropTypes.func,
|
|
||||||
loadSavedForm: PropTypes.func,
|
|
||||||
push: PropTypes.func,
|
|
||||||
saveForm: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadSavedForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(job) {
|
|
||||||
this.props.saveForm(job);
|
|
||||||
this.props.push('/jobs/new/preview');
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCertClick(name) {
|
|
||||||
const { fields } = this.props;
|
|
||||||
Object.keys(certTypes).forEach(certType => {
|
|
||||||
if (certType === name) {
|
|
||||||
return fields[certType].onChange(true);
|
|
||||||
}
|
|
||||||
return fields[certType].onChange(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
fields: {
|
|
||||||
position,
|
|
||||||
locale,
|
|
||||||
description,
|
|
||||||
email,
|
|
||||||
url,
|
|
||||||
logo,
|
|
||||||
company,
|
|
||||||
isHighlighted,
|
|
||||||
isRemoteOk,
|
|
||||||
howToApply,
|
|
||||||
isFrontEndCert,
|
|
||||||
isBackEndCert
|
|
||||||
},
|
|
||||||
handleSubmit
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { handleChange } = this;
|
|
||||||
const labelClass = 'col-sm-offset-1 col-sm-2';
|
|
||||||
const inputClass = 'col-sm-6';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 10 }
|
|
||||||
mdOffset={ 1 }>
|
|
||||||
<div className='text-center'>
|
|
||||||
<form
|
|
||||||
className='form-horizontal'
|
|
||||||
onSubmit={ handleSubmit(data => this.handleSubmit(data)) }>
|
|
||||||
|
|
||||||
<div className='spacer'>
|
|
||||||
<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={ getValidationState(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={ getValidationState(locale) }
|
|
||||||
label='Location'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
placeholder='e.g. San Francisco, Remote, etc.'
|
|
||||||
required={ true }
|
|
||||||
type='text'
|
|
||||||
wrapperClassName={ inputClass }
|
|
||||||
{ ...locale }
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
bsStyle={ getValidationState(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>
|
|
||||||
<Input
|
|
||||||
bsStyle={ getValidationState(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={ getValidationState(company) }
|
|
||||||
label='Company Name'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
onChange={ (e) => handleChange('company', e) }
|
|
||||||
type='text'
|
|
||||||
wrapperClassName={ inputClass }
|
|
||||||
{ ...company }
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
bsStyle={ getValidationState(email) }
|
|
||||||
label='Email'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
placeholder='This is how we will contact you'
|
|
||||||
required={ true }
|
|
||||||
type='email'
|
|
||||||
wrapperClassName={ inputClass }
|
|
||||||
{ ...email }
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
bsStyle={ getValidationState(url) }
|
|
||||||
label='URL'
|
|
||||||
labelClassName={ labelClass }
|
|
||||||
placeholder='http://yourcompany.com'
|
|
||||||
type='url'
|
|
||||||
wrapperClassName={ inputClass }
|
|
||||||
{ ...url }
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
bsStyle={ getValidationState(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>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }>
|
|
||||||
Highlight this ad to give it extra attention.
|
|
||||||
<br />
|
|
||||||
Featured listings receive more clicks and more applications.
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<Row>
|
|
||||||
<Input
|
|
||||||
bsSize='large'
|
|
||||||
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: createFormValidator(fieldValidators)
|
|
||||||
},
|
|
||||||
state => ({ initialValues: state.jobsApp.initialValues }),
|
|
||||||
{
|
|
||||||
loadSavedForm,
|
|
||||||
push,
|
|
||||||
saveForm
|
|
||||||
}
|
|
||||||
)(NewJob);
|
|
@ -1,43 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { LinkContainer } from 'react-router-bootstrap';
|
|
||||||
import { Button, Col, Row } from 'react-bootstrap';
|
|
||||||
|
|
||||||
export default class extends React.createClass {
|
|
||||||
static displayName = 'NewJobCompleted';
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className='text-center'>
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<h1>
|
|
||||||
Your Position has Been Submitted
|
|
||||||
</h1>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }>
|
|
||||||
We’ll review your listing and email you when it’s live.
|
|
||||||
<br />
|
|
||||||
Thank you for listing this job with Free Code Camp.
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<LinkContainer to={ '/jobs' }>
|
|
||||||
<Button
|
|
||||||
block={ true }
|
|
||||||
bsSize='large'
|
|
||||||
bsStyle='primary'>
|
|
||||||
Go to the job board
|
|
||||||
</Button>
|
|
||||||
</LinkContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
import { CompositeDisposable } from 'rx';
|
|
||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
|
||||||
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 JobNotFound from './JobNotFound.jsx';
|
|
||||||
|
|
||||||
import { clearForm, saveJob } from '../redux/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({ job: state.jobsApp.newJob });
|
|
||||||
|
|
||||||
const bindableActions = {
|
|
||||||
goBack,
|
|
||||||
push,
|
|
||||||
clearForm,
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this._subscriptions.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleJobSubmit() {
|
|
||||||
const { clearForm, saveJob, job } = this.props;
|
|
||||||
clearForm();
|
|
||||||
const subscription = saveJob(job).subscribe();
|
|
||||||
this._subscriptions.add(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { job, goBack } = this.props;
|
|
||||||
|
|
||||||
if (!job || !job.position || !job.description) {
|
|
||||||
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={ () => 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,146 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { compose } from 'redux';
|
|
||||||
import { contain } from 'redux-epic';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { push } from 'react-router-redux';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import { fetchJobs } from '../redux/actions';
|
|
||||||
|
|
||||||
import ShowJob from './ShowJob.jsx';
|
|
||||||
import JobNotFound from './JobNotFound.jsx';
|
|
||||||
import { isJobValid } from '../utils';
|
|
||||||
|
|
||||||
function shouldShowApply(
|
|
||||||
{
|
|
||||||
isFrontEndCert: isFrontEndCertReq = false,
|
|
||||||
isBackEndCert: isBackEndCertReq = false
|
|
||||||
}, {
|
|
||||||
isFrontEndCert = false,
|
|
||||||
isBackEndCert = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return (!isFrontEndCertReq && !isBackEndCertReq) ||
|
|
||||||
(isBackEndCertReq && isBackEndCert) ||
|
|
||||||
(isFrontEndCertReq && isFrontEndCert);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateMessage(
|
|
||||||
{
|
|
||||||
isFrontEndCert: isFrontEndCertReq = false,
|
|
||||||
isBackEndCert: isBackEndCertReq = false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isFrontEndCert = false,
|
|
||||||
isBackEndCert = false,
|
|
||||||
isSignedIn = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
if (!isSignedIn) {
|
|
||||||
return 'Must be signed in to apply';
|
|
||||||
}
|
|
||||||
if (isFrontEndCertReq && !isFrontEndCert) {
|
|
||||||
return 'This employer requires Free Code Camp’s Front ' +
|
|
||||||
'End Development Certification in order to apply';
|
|
||||||
}
|
|
||||||
if (isBackEndCertReq && !isBackEndCert) {
|
|
||||||
return 'This employer requires Free Code Camp’s Back ' +
|
|
||||||
'End Development Certification in order to apply';
|
|
||||||
}
|
|
||||||
if (isFrontEndCertReq && isFrontEndCertReq) {
|
|
||||||
return 'This employer requires the Front End Development Certification. ' +
|
|
||||||
"You've earned it, so feel free to apply.";
|
|
||||||
}
|
|
||||||
return 'This employer requires the Back End Development Certification. ' +
|
|
||||||
"You've earned it, so feel free to apply.";
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
state => state.app,
|
|
||||||
state => state.jobsApp.currentJob,
|
|
||||||
state => state.entities.job,
|
|
||||||
({ username, isFrontEndCert, isBackEndCert }, currentJob, jobMap) => ({
|
|
||||||
username,
|
|
||||||
isFrontEndCert,
|
|
||||||
isBackEndCert,
|
|
||||||
job: jobMap[currentJob] || {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
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,147 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { Row, Col, Thumbnail } from 'react-bootstrap';
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
|
||||||
import urlRegexFactory from 'url-regex';
|
|
||||||
|
|
||||||
const urlRegex = urlRegexFactory();
|
|
||||||
const defaultImage =
|
|
||||||
'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png';
|
|
||||||
|
|
||||||
const thumbnailStyle = {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
maxHeight: '100px',
|
|
||||||
maxWidth: '100px'
|
|
||||||
};
|
|
||||||
|
|
||||||
function addATags(text) {
|
|
||||||
return text.replace(urlRegex, function(match) {
|
|
||||||
return `<a href=${match} target='_blank'>${match}</a>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class extends PureComponent {
|
|
||||||
static displayName = 'ShowJob';
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
job: PropTypes.object,
|
|
||||||
params: PropTypes.object,
|
|
||||||
showApply: PropTypes.bool,
|
|
||||||
preview: PropTypes.bool,
|
|
||||||
message: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
renderHeader({ company, position }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
|
|
||||||
<h5
|
|
||||||
className='pull-right hidden-xs hidden-md'
|
|
||||||
style={{ display: 'inline-block' }}>
|
|
||||||
{ position }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHowToApply(showApply, preview, message, howToApply) {
|
|
||||||
if (!showApply) {
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }>
|
|
||||||
<h4 className='bg-info text-center'>{ message }</h4>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<hr />
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }>
|
|
||||||
<div>
|
|
||||||
<bold>{ preview ? 'How do I apply?' : message }</bold>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<span dangerouslySetInnerHTML={{
|
|
||||||
__html: addATags(howToApply)
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
showApply = true,
|
|
||||||
message,
|
|
||||||
preview = true,
|
|
||||||
job = {}
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
logo,
|
|
||||||
position,
|
|
||||||
city,
|
|
||||||
company,
|
|
||||||
state,
|
|
||||||
locale,
|
|
||||||
description,
|
|
||||||
howToApply
|
|
||||||
} = job;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 10 }
|
|
||||||
mdOffset={ 1 }
|
|
||||||
xs={ 12 }>
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<h2 className='text-center'>
|
|
||||||
{ company }
|
|
||||||
</h2>
|
|
||||||
</Row>
|
|
||||||
<div className='spacer' />
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 2 }
|
|
||||||
mdOffset={ 3 }>
|
|
||||||
<Thumbnail
|
|
||||||
alt={ logo ? company + 'company logo' : 'stock image' }
|
|
||||||
src={ logo || defaultImage }
|
|
||||||
style={ thumbnailStyle } />
|
|
||||||
</Col>
|
|
||||||
<Col
|
|
||||||
md={ 4 }>
|
|
||||||
|
|
||||||
<bold>Position: </bold> { position || 'N/A' }
|
|
||||||
<br />
|
|
||||||
<bold>Location: </bold>
|
|
||||||
{ locale ? locale : `${city}, ${state}` }
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<hr />
|
|
||||||
<div className='spacer' />
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
md={ 6 }
|
|
||||||
mdOffset={ 3 }
|
|
||||||
style={{ whiteSpace: 'pre-line' }}
|
|
||||||
xs={ 12 }>
|
|
||||||
<p>{ description }</p>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
{ this.renderHowToApply(showApply, preview, message, howToApply) }
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
export default {
|
|
||||||
getChildRoutes: (_, cb) => {
|
|
||||||
require.ensure(
|
|
||||||
[
|
|
||||||
'./components/Jobs.jsx',
|
|
||||||
'./components/NewJob.jsx',
|
|
||||||
'./components/Preview.jsx',
|
|
||||||
'./components/JobTotal.jsx',
|
|
||||||
'./components/NewJobCompleted.jsx',
|
|
||||||
'./components/Show.jsx'
|
|
||||||
],
|
|
||||||
require => {
|
|
||||||
cb(null, [{
|
|
||||||
path: '/jobs',
|
|
||||||
component: require('./components/Jobs.jsx').default
|
|
||||||
}, {
|
|
||||||
path: 'jobs/new',
|
|
||||||
component: require('./components/NewJob.jsx').default
|
|
||||||
}, {
|
|
||||||
path: 'jobs/new/preview',
|
|
||||||
component: require('./components/Preview.jsx').default
|
|
||||||
}, {
|
|
||||||
path: 'jobs/new/check-out',
|
|
||||||
component: require('./components/JobTotal.jsx').default
|
|
||||||
}, {
|
|
||||||
path: 'jobs/new/completed',
|
|
||||||
component: require('./components/NewJobCompleted.jsx').default
|
|
||||||
}, {
|
|
||||||
path: 'jobs/:id',
|
|
||||||
component: require('./components/Show.jsx').default
|
|
||||||
}]);
|
|
||||||
},
|
|
||||||
'jobs'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,35 +0,0 @@
|
|||||||
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 }),
|
|
||||||
entities => ({ entities })
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
@ -1,33 +0,0 @@
|
|||||||
import { Observable } from 'rx';
|
|
||||||
|
|
||||||
import { applyPromo } from './types';
|
|
||||||
import { applyPromoCompleted } from './actions';
|
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
|
||||||
|
|
||||||
export default function applyPromoSaga(action$) {
|
|
||||||
return action$
|
|
||||||
.filter(action => action.type === applyPromo)
|
|
||||||
.flatMap(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
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
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 function fetchJobsSaga(action$, getState, { services }) {
|
|
||||||
return action$
|
|
||||||
.filter(action => action.type === fetchJobs)
|
|
||||||
.flatMap(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(
|
|
||||||
entities,
|
|
||||||
result.jobs[0],
|
|
||||||
result.jobs
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(error => Observable.just({ type: handleError, error }));
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
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 ];
|
|
@ -1,19 +0,0 @@
|
|||||||
import {
|
|
||||||
inHTMLData,
|
|
||||||
uriInSingleQuotedAttr
|
|
||||||
} from 'xss-filters';
|
|
||||||
|
|
||||||
import { callIfDefined, formatUrl } from '../../../utils/form';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
NewJob: {
|
|
||||||
position: callIfDefined(inHTMLData),
|
|
||||||
locale: callIfDefined(inHTMLData),
|
|
||||||
description: callIfDefined(inHTMLData),
|
|
||||||
email: callIfDefined(inHTMLData),
|
|
||||||
url: callIfDefined(value => formatUrl(uriInSingleQuotedAttr(value))),
|
|
||||||
logo: callIfDefined(value => formatUrl(uriInSingleQuotedAttr(value))),
|
|
||||||
company: callIfDefined(inHTMLData),
|
|
||||||
howToApply: callIfDefined(inHTMLData)
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,79 +0,0 @@
|
|||||||
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: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default handleActions(
|
|
||||||
{
|
|
||||||
[types.findJob]: (state, { payload: id }) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentJob: id
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[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
|
|
||||||
);
|
|
@ -1,25 +0,0 @@
|
|||||||
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 function saveJobSaga(action$, getState, { services }) {
|
|
||||||
return action$
|
|
||||||
.filter(action => action.type === saveJob)
|
|
||||||
.flatMap(action => {
|
|
||||||
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
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import createTypes from '../../../utils/create-types';
|
|
||||||
|
|
||||||
export default createTypes([
|
|
||||||
'fetchJobs',
|
|
||||||
'fetchJobsCompleted',
|
|
||||||
|
|
||||||
'findJob',
|
|
||||||
'saveJob',
|
|
||||||
'saveForm',
|
|
||||||
|
|
||||||
'saveCompleted',
|
|
||||||
|
|
||||||
'clearForm',
|
|
||||||
|
|
||||||
'loadSavedForm',
|
|
||||||
'loadSavedFormCompleted',
|
|
||||||
|
|
||||||
'clearPromo',
|
|
||||||
'updatePromo',
|
|
||||||
'applyPromo',
|
|
||||||
'applyPromoCompleted'
|
|
||||||
], 'jobs');
|
|
@ -1,29 +0,0 @@
|
|||||||
const defaults = {
|
|
||||||
string: {
|
|
||||||
value: '',
|
|
||||||
valid: false,
|
|
||||||
pristine: true,
|
|
||||||
type: 'string'
|
|
||||||
},
|
|
||||||
bool: {
|
|
||||||
value: false,
|
|
||||||
type: 'boolean'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getDefaults(type, value) {
|
|
||||||
if (!type) {
|
|
||||||
return defaults['string'];
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
return Object.assign({}, defaults[type], { value });
|
|
||||||
}
|
|
||||||
return Object.assign({}, defaults[type]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isJobValid(job) {
|
|
||||||
return job &&
|
|
||||||
!job.isFilled &&
|
|
||||||
job.isApproved &&
|
|
||||||
job.isPaid;
|
|
||||||
}
|
|
@ -8,6 +8,7 @@ import PureComponent from 'react-pure-render/component';
|
|||||||
import Classic from './classic/Classic.jsx';
|
import Classic from './classic/Classic.jsx';
|
||||||
import Step from './step/Step.jsx';
|
import Step from './step/Step.jsx';
|
||||||
import Project from './project/Project.jsx';
|
import Project from './project/Project.jsx';
|
||||||
|
import Video from './video/Video.jsx';
|
||||||
|
|
||||||
import { fetchChallenge, fetchChallenges } from '../redux/actions';
|
import { fetchChallenge, fetchChallenges } from '../redux/actions';
|
||||||
import { challengeSelector } from '../redux/selectors';
|
import { challengeSelector } from '../redux/selectors';
|
||||||
@ -16,7 +17,8 @@ const views = {
|
|||||||
step: Step,
|
step: Step,
|
||||||
classic: Classic,
|
classic: Classic,
|
||||||
project: Project,
|
project: Project,
|
||||||
simple: Project
|
simple: Project,
|
||||||
|
video: Video
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindableActions = {
|
const bindableActions = {
|
||||||
|
110
common/app/routes/challenges/components/video/Lecture.jsx
Normal file
110
common/app/routes/challenges/components/video/Lecture.jsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Button, Col, Row } from 'react-bootstrap';
|
||||||
|
import Youtube from 'react-youtube';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
import { toggleQuestionView } from '../../redux/actions';
|
||||||
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
|
|
||||||
|
const log = debug('fcc:videos');
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
challengeSelector,
|
||||||
|
({
|
||||||
|
challenge: {
|
||||||
|
id = 'foo',
|
||||||
|
dashedName,
|
||||||
|
description,
|
||||||
|
challengeSeed: [ videoId ] = [ '1' ]
|
||||||
|
}
|
||||||
|
}) => ({
|
||||||
|
id,
|
||||||
|
videoId,
|
||||||
|
dashedName,
|
||||||
|
description
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export class Lecture extends React.Component {
|
||||||
|
static displayName = 'Lecture';
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
// actions
|
||||||
|
toggleQuestionView: PropTypes.func,
|
||||||
|
// ui
|
||||||
|
id: PropTypes.string,
|
||||||
|
videoId: PropTypes.string,
|
||||||
|
description: PropTypes.array,
|
||||||
|
dashedName: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
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'
|
||||||
|
dangerouslySetInnerHTML={{__html: line}}
|
||||||
|
key={ dashedName + index }
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
videoId,
|
||||||
|
description = [],
|
||||||
|
toggleQuestionView
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const dashedName = 'foo';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col xs={ 12 }>
|
||||||
|
<Row>
|
||||||
|
<div className='embed-responsive embed-responsive-16by9'>
|
||||||
|
<Youtube
|
||||||
|
className='embed-responsive-item'
|
||||||
|
id={ id }
|
||||||
|
onError={ this.handleError }
|
||||||
|
videoId={ videoId }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }
|
||||||
|
xs={ 12 }
|
||||||
|
>
|
||||||
|
<article>
|
||||||
|
{ this.renderTranscript(description, dashedName) }
|
||||||
|
</article>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={ toggleQuestionView }
|
||||||
|
>
|
||||||
|
Take me to the Questions
|
||||||
|
</Button>
|
||||||
|
<div className='spacer' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ toggleQuestionView }
|
||||||
|
)(Lecture);
|
@ -9,8 +9,8 @@ import {
|
|||||||
moveQuestion,
|
moveQuestion,
|
||||||
releaseQuestion,
|
releaseQuestion,
|
||||||
grabQuestion
|
grabQuestion
|
||||||
} from '../redux/actions';
|
} from '../../redux/actions';
|
||||||
import { getCurrentHike } from '../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
|
|
||||||
const answerThreshold = 100;
|
const answerThreshold = 100;
|
||||||
const springProperties = { stiffness: 120, damping: 10 };
|
const springProperties = { stiffness: 120, damping: 10 };
|
||||||
@ -22,24 +22,21 @@ const actionsToBind = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
getCurrentHike,
|
challengeSelector,
|
||||||
state => state.hikesApp,
|
state => state.challengesApp,
|
||||||
state => state.app.isSignedIn,
|
state => state.app.isSignedIn,
|
||||||
(currentHike, ui, isSignedIn) => {
|
(
|
||||||
const {
|
{ challenge: { tests = [ ] }},
|
||||||
|
{
|
||||||
currentQuestion = 1,
|
currentQuestion = 1,
|
||||||
mouse = [ 0, 0 ],
|
mouse = [ 0, 0 ],
|
||||||
delta = [ 0, 0 ],
|
delta = [ 0, 0 ],
|
||||||
isCorrect = false,
|
isCorrect = false,
|
||||||
isPressed = false,
|
isPressed = false,
|
||||||
shouldShakeQuestion = false
|
shouldShakeQuestion = false
|
||||||
} = ui;
|
},
|
||||||
|
isSignedIn
|
||||||
const {
|
) => ({
|
||||||
tests = []
|
|
||||||
} = currentHike;
|
|
||||||
|
|
||||||
return {
|
|
||||||
tests,
|
tests,
|
||||||
currentQuestion,
|
currentQuestion,
|
||||||
isCorrect,
|
isCorrect,
|
||||||
@ -48,8 +45,7 @@ const mapStateToProps = createSelector(
|
|||||||
isPressed,
|
isPressed,
|
||||||
shouldShakeQuestion,
|
shouldShakeQuestion,
|
||||||
isSignedIn
|
isSignedIn
|
||||||
};
|
})
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
class Question extends React.Component {
|
class Question extends React.Component {
|
||||||
@ -133,7 +129,8 @@ class Question extends React.Component {
|
|||||||
onTouchEnd={ mouseUp }
|
onTouchEnd={ mouseUp }
|
||||||
onTouchMove={ this.handleMouseMove(isPressed, this.props) }
|
onTouchMove={ this.handleMouseMove(isPressed, this.props) }
|
||||||
onTouchStart={ grabQuestion }
|
onTouchStart={ grabQuestion }
|
||||||
style={ style }>
|
style={ style }
|
||||||
|
>
|
||||||
<h4>Question { number }</h4>
|
<h4>Question { number }</h4>
|
||||||
<p>{ question }</p>
|
<p>{ question }</p>
|
||||||
</article>
|
</article>
|
||||||
@ -162,7 +159,8 @@ class Question extends React.Component {
|
|||||||
<Col
|
<Col
|
||||||
onMouseUp={ e => this.handleMouseUp(e, answer, info) }
|
onMouseUp={ e => this.handleMouseUp(e, answer, info) }
|
||||||
xs={ 8 }
|
xs={ 8 }
|
||||||
xsOffset={ 2 }>
|
xsOffset={ 2 }
|
||||||
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<Motion style={{ x: spring(xPosition, springProperties) }}>
|
<Motion style={{ x: spring(xPosition, springProperties) }}>
|
||||||
{ questionElement }
|
{ questionElement }
|
||||||
@ -174,14 +172,16 @@ class Question extends React.Component {
|
|||||||
bsSize='large'
|
bsSize='large'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='pull-left'
|
className='pull-left'
|
||||||
onClick={ this.onAnswer(answer, false, info) }>
|
onClick={ this.onAnswer(answer, false, info) }
|
||||||
|
>
|
||||||
false
|
false
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
bsSize='large'
|
bsSize='large'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='pull-right'
|
className='pull-right'
|
||||||
onClick={ this.onAnswer(answer, true, info) }>
|
onClick={ this.onAnswer(answer, true, info) }
|
||||||
|
>
|
||||||
true
|
true
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
@ -5,26 +5,27 @@ 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 { resetUi } from '../../redux/actions';
|
||||||
import { updateTitle } from '../../../redux/actions';
|
import { updateTitle } from '../../../../redux/actions';
|
||||||
import { getCurrentHike } from '../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
|
|
||||||
|
const bindableActions = { resetUi, updateTitle };
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
getCurrentHike,
|
challengeSelector,
|
||||||
state => state.hikesApp.shouldShowQuestions,
|
state => state.challengesApp.shouldShowQuestions,
|
||||||
(currentHike, shouldShowQuestions) => ({
|
({ challenge: { title } }, shouldShowQuestions) => ({
|
||||||
title: currentHike ? currentHike.title : '',
|
title,
|
||||||
shouldShowQuestions
|
shouldShowQuestions
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// export plain component for testing
|
// export plain component for testing
|
||||||
export class Hike extends React.Component {
|
export class Video extends React.Component {
|
||||||
static displayName = 'Hike';
|
static displayName = 'Video';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// actions
|
// actions
|
||||||
resetHike: PropTypes.func,
|
resetUi: PropTypes.func,
|
||||||
// ui
|
// ui
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
@ -38,12 +39,12 @@ export class Hike extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.resetHike();
|
this.props.resetUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps({ params: { dashedName } }) {
|
componentWillReceiveProps({ title }) {
|
||||||
if (this.props.params.dashedName !== dashedName) {
|
if (this.props.title !== title) {
|
||||||
this.props.resetHike();
|
this.props.resetUi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +70,8 @@ export class Hike extends React.Component {
|
|||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
<section
|
<section
|
||||||
className={ 'text-center' }
|
className={ 'text-center' }
|
||||||
title={ title }>
|
title={ title }
|
||||||
|
>
|
||||||
{ this.renderBody(shouldShowQuestions) }
|
{ this.renderBody(shouldShowQuestions) }
|
||||||
</section>
|
</section>
|
||||||
</Col>
|
</Col>
|
||||||
@ -78,4 +80,7 @@ export class Hike extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// export redux aware component
|
// export redux aware component
|
||||||
export default connect(mapStateToProps, { resetHike, updateTitle })(Hike);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
bindableActions
|
||||||
|
)(Video);
|
@ -1,6 +1,6 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { updateContents } from '../../../../utils/polyvinyl';
|
import { updateContents } from '../../../../utils/polyvinyl';
|
||||||
import { loggerToStr } from '../utils';
|
import { getMouse, loggerToStr } from '../utils';
|
||||||
|
|
||||||
import types from './types';
|
import types from './types';
|
||||||
|
|
||||||
@ -79,3 +79,40 @@ export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
|||||||
export const saveCode = createAction(types.saveCode);
|
export const saveCode = createAction(types.saveCode);
|
||||||
export const loadCode = createAction(types.loadCode);
|
export const loadCode = createAction(types.loadCode);
|
||||||
export const savedCodeFound = createAction(types.savedCodeFound);
|
export const savedCodeFound = createAction(types.savedCodeFound);
|
||||||
|
|
||||||
|
|
||||||
|
// video challenges
|
||||||
|
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 videoCompleted = createAction(types.videoCompleted);
|
||||||
|
86
common/app/routes/challenges/redux/answer-saga.js
Normal file
86
common/app/routes/challenges/redux/answer-saga.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
import types from './types';
|
||||||
|
import { getMouse } from '../utils';
|
||||||
|
|
||||||
|
import { submitChallenge, videoCompleted } from './actions';
|
||||||
|
import { createErrorObservable, makeToast } from '../../../redux/actions';
|
||||||
|
import { challengeSelector } from './selectors';
|
||||||
|
|
||||||
|
export default function answerSaga(action$, getState) {
|
||||||
|
return action$
|
||||||
|
.filter(action => action.type === types.answerQuestion)
|
||||||
|
.flatMap(({
|
||||||
|
payload: {
|
||||||
|
e,
|
||||||
|
answer,
|
||||||
|
userAnswer,
|
||||||
|
info,
|
||||||
|
threshold
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const state = getState();
|
||||||
|
const {
|
||||||
|
challenge: { tests }
|
||||||
|
} = challengeSelector(state);
|
||||||
|
const {
|
||||||
|
challengesApp: {
|
||||||
|
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 Observable.just(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionX >= threshold) {
|
||||||
|
finalAnswer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionX <= -threshold) {
|
||||||
|
finalAnswer = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalAnswer = userAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// incorrect question
|
||||||
|
if (answer !== finalAnswer) {
|
||||||
|
let infoAction;
|
||||||
|
if (info) {
|
||||||
|
infoAction = makeToast({
|
||||||
|
title: 'Have a hint',
|
||||||
|
message: info,
|
||||||
|
type: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable
|
||||||
|
.just({ type: types.endShake })
|
||||||
|
.delay(500)
|
||||||
|
.startWith(infoAction, { type: types.startShake });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tests[currentQuestion]) {
|
||||||
|
return Observable
|
||||||
|
.just({ type: types.goToNextQuestion })
|
||||||
|
.delay(300)
|
||||||
|
.startWith({ type: types.primeNextQuestion });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Observable.just(submitChallenge())
|
||||||
|
.delay(300)
|
||||||
|
// moves question to the appropriate side of the screen
|
||||||
|
.startWith(videoCompleted(finalAnswer))
|
||||||
|
// end with action so we know it is ok to transition
|
||||||
|
.concat(Observable.just({ type: types.transitionHike }))
|
||||||
|
.catch(createErrorObservable);
|
||||||
|
});
|
||||||
|
}
|
@ -12,6 +12,9 @@ import { backEndProject } from '../../../utils/challengeTypes';
|
|||||||
import { randomCompliment } from '../../../utils/get-words';
|
import { randomCompliment } from '../../../utils/get-words';
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
|
||||||
|
// NOTE(@BerkeleyTrue): this file could benefit from some refactoring.
|
||||||
|
// lots of repeat code
|
||||||
|
|
||||||
function completedChallenge(state) {
|
function completedChallenge(state) {
|
||||||
let body;
|
let body;
|
||||||
let isSignedIn = false;
|
let isSignedIn = false;
|
||||||
@ -163,6 +166,7 @@ function submitSimpleChallenge(type, state) {
|
|||||||
const submitTypes = {
|
const submitTypes = {
|
||||||
tests: submitModern,
|
tests: submitModern,
|
||||||
step: submitSimpleChallenge,
|
step: submitSimpleChallenge,
|
||||||
|
video: submitSimpleChallenge,
|
||||||
'project.frontEnd': submitProject,
|
'project.frontEnd': submitProject,
|
||||||
'project.backEnd': submitProject,
|
'project.backEnd': submitProject,
|
||||||
'project.simple': submitSimpleChallenge
|
'project.simple': submitSimpleChallenge
|
||||||
|
@ -5,11 +5,13 @@ export types from './types';
|
|||||||
import fetchChallengesSaga from './fetch-challenges-saga';
|
import fetchChallengesSaga from './fetch-challenges-saga';
|
||||||
import completionSaga from './completion-saga';
|
import completionSaga from './completion-saga';
|
||||||
import nextChallengeSaga from './next-challenge-saga';
|
import nextChallengeSaga from './next-challenge-saga';
|
||||||
|
import answerSaga from './answer-saga';
|
||||||
|
|
||||||
export projectNormalizer from './project-normalizer';
|
export projectNormalizer from './project-normalizer';
|
||||||
|
|
||||||
export const sagas = [
|
export const sagas = [
|
||||||
fetchChallengesSaga,
|
fetchChallengesSaga,
|
||||||
completionSaga,
|
completionSaga,
|
||||||
nextChallengeSaga
|
nextChallengeSaga,
|
||||||
|
answerSaga
|
||||||
];
|
];
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
getFirstChallengeOfNextBlock,
|
getFirstChallengeOfNextBlock,
|
||||||
getFirstChallengeOfNextSuperBlock
|
getFirstChallengeOfNextSuperBlock
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { getRandomVerb } from '../../../utils/get-words';
|
import { randomVerb } from '../../../utils/get-words';
|
||||||
|
|
||||||
export default function nextChallengeSaga(actions$, getState) {
|
export default function nextChallengeSaga(actions$, getState) {
|
||||||
return actions$
|
return actions$
|
||||||
@ -48,7 +48,7 @@ export default function nextChallengeSaga(actions$, getState) {
|
|||||||
}
|
}
|
||||||
message += ' Your next challenge has arrived.';
|
message += ' Your next challenge has arrived.';
|
||||||
const toast = {
|
const toast = {
|
||||||
// title: isNewSuperBlock || isNewBlock ? getRandomVerb() : null,
|
// title: isNewSuperBlock || isNewBlock ? randomVerb() : null,
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
*/
|
*/
|
||||||
@ -56,7 +56,7 @@ export default function nextChallengeSaga(actions$, getState) {
|
|||||||
updateCurrentChallenge(nextChallenge),
|
updateCurrentChallenge(nextChallenge),
|
||||||
resetUi(),
|
resetUi(),
|
||||||
makeToast({
|
makeToast({
|
||||||
title: getRandomVerb(),
|
title: randomVerb(),
|
||||||
message: 'Your next challenge has arrived.'
|
message: 'Your next challenge has arrived.'
|
||||||
}),
|
}),
|
||||||
push(`/challenges/${nextChallenge.block}/${nextChallenge.dashedName}`)
|
push(`/challenges/${nextChallenge.block}/${nextChallenge.dashedName}`)
|
||||||
|
@ -12,15 +12,30 @@ import {
|
|||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
const initialUiState = {
|
const initialUiState = {
|
||||||
|
// step index tracing
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
previousIndex: -1,
|
previousIndex: -1,
|
||||||
|
// step action
|
||||||
isActionCompleted: false,
|
isActionCompleted: false,
|
||||||
isSubmitting: true,
|
// project is ready to submit
|
||||||
|
isSubmitting: false,
|
||||||
output: `/**
|
output: `/**
|
||||||
* Any console.log()
|
* Any console.log()
|
||||||
* statements will appear in
|
* statements will appear in
|
||||||
* here console.
|
* here console.
|
||||||
*/`
|
*/`,
|
||||||
|
// video
|
||||||
|
// 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
|
||||||
};
|
};
|
||||||
const initialState = {
|
const initialState = {
|
||||||
id: '',
|
id: '',
|
||||||
@ -107,6 +122,49 @@ const mainReducer = handleActions(
|
|||||||
[types.updateOutput]: (state, { payload: output }) => ({
|
[types.updateOutput]: (state, { payload: output }) => ({
|
||||||
...state,
|
...state,
|
||||||
output: (state.output || '') + output
|
output: (state.output || '') + output
|
||||||
|
}),
|
||||||
|
// video
|
||||||
|
[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.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.videoCompleted]: (state, { payload: userAnswer } ) => ({
|
||||||
|
...state,
|
||||||
|
isCorrect: true,
|
||||||
|
isPressed: false,
|
||||||
|
delta: [ 0, 0 ],
|
||||||
|
mouse: [ userAnswer ? 1000 : -1000, 0]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
|
@ -39,5 +39,21 @@ export default createTypes([
|
|||||||
// code storage
|
// code storage
|
||||||
'saveCode',
|
'saveCode',
|
||||||
'loadCode',
|
'loadCode',
|
||||||
'savedCodeFound'
|
'savedCodeFound',
|
||||||
|
|
||||||
|
// video challenges
|
||||||
|
'toggleQuestionView',
|
||||||
|
'grabQuestion',
|
||||||
|
'releaseQuestion',
|
||||||
|
'moveQuestion',
|
||||||
|
|
||||||
|
'answerQuestion',
|
||||||
|
|
||||||
|
'startShake',
|
||||||
|
'endShake',
|
||||||
|
|
||||||
|
'primeNextQuestion',
|
||||||
|
'goToNextQuestion',
|
||||||
|
'transitionVideo',
|
||||||
|
'videoCompleted'
|
||||||
], 'challenges');
|
], 'challenges');
|
||||||
|
@ -168,3 +168,21 @@ export function getCurrentSuperBlockName(current, entities) {
|
|||||||
const block = blockMap[challenge.block];
|
const block = blockMap[challenge.block];
|
||||||
return block.superBlock;
|
return block.superBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gets new mouse position
|
||||||
|
// getMouse(
|
||||||
|
// e: MouseEvent|TouchEvent,
|
||||||
|
// [ dx: Number, dy: Number ]
|
||||||
|
// ) => [ Number, Number ]
|
||||||
|
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,13 +1,9 @@
|
|||||||
import Jobs from './Jobs';
|
|
||||||
import Hikes from './Hikes';
|
|
||||||
import { modernChallenges, map, challenges } from './challenges';
|
import { modernChallenges, map, challenges } from './challenges';
|
||||||
import NotFound from '../components/NotFound/index.jsx';
|
import NotFound from '../components/NotFound/index.jsx';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/',
|
path: '/',
|
||||||
childRoutes: [
|
childRoutes: [
|
||||||
Jobs,
|
|
||||||
Hikes,
|
|
||||||
challenges,
|
challenges,
|
||||||
modernChallenges,
|
modernChallenges,
|
||||||
map,
|
map,
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import { sagas as appSagas } from './redux';
|
import { sagas as appSagas } from './redux';
|
||||||
import { sagas as hikesSagas} from './routes/Hikes/redux';
|
|
||||||
import { sagas as jobsSagas } from './routes/Jobs/redux';
|
|
||||||
import { sagas as challengeSagas } from './routes/challenges/redux';
|
import { sagas as challengeSagas } from './routes/challenges/redux';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...appSagas,
|
...appSagas,
|
||||||
...hikesSagas,
|
|
||||||
...jobsSagas,
|
|
||||||
...challengeSagas
|
...challengeSagas
|
||||||
];
|
];
|
||||||
|
Reference in New Issue
Block a user