Move Video challenges under challenges dir
Remove old hikes components Remove unused jobs stuffpull/7430/head
parent
5f5f9e526e
commit
4a043e151e
|
@ -1,6 +1,5 @@
|
|||
import errSaga from './err-saga';
|
||||
import titleSaga from './title-saga';
|
||||
import localStorageSaga from './local-storage-saga';
|
||||
import hardGoToSaga from './hard-go-to-saga';
|
||||
import windowSaga from './window-saga';
|
||||
import executeChallengeSaga from './execute-challenge-saga';
|
||||
|
@ -11,7 +10,6 @@ import gitterSaga from './gitter-saga';
|
|||
export default [
|
||||
errSaga,
|
||||
titleSaga,
|
||||
localStorageSaga,
|
||||
hardGoToSaga,
|
||||
windowSaga,
|
||||
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 entitiesReducer from './redux/entities-reducer';
|
||||
import { reducer as hikesApp } from './routes/Hikes/redux';
|
||||
import {
|
||||
reducer as challengesApp,
|
||||
projectNormalizer
|
||||
} from './routes/challenges/redux';
|
||||
import {
|
||||
reducer as jobsApp,
|
||||
formNormalizer as jobsNormalizer
|
||||
} from './routes/Jobs/redux';
|
||||
|
||||
export default function createReducer(sideReducers = {}) {
|
||||
return combineReducers({
|
||||
...sideReducers,
|
||||
entities: entitiesReducer,
|
||||
app,
|
||||
hikesApp,
|
||||
jobsApp,
|
||||
challengesApp,
|
||||
form: formReducer.normalize({
|
||||
...jobsNormalizer,
|
||||
...projectNormalizer
|
||||
})
|
||||
form: formReducer.normalize({ ...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 Step from './step/Step.jsx';
|
||||
import Project from './project/Project.jsx';
|
||||
import Video from './video/Video.jsx';
|
||||
|
||||
import { fetchChallenge, fetchChallenges } from '../redux/actions';
|
||||
import { challengeSelector } from '../redux/selectors';
|
||||
|
@ -16,7 +17,8 @@ const views = {
|
|||
step: Step,
|
||||
classic: Classic,
|
||||
project: Project,
|
||||
simple: Project
|
||||
simple: Project,
|
||||
video: Video
|
||||
};
|
||||
|
||||
const bindableActions = {
|
||||
|
|
|
@ -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,
|
||||
releaseQuestion,
|
||||
grabQuestion
|
||||
} from '../redux/actions';
|
||||
import { getCurrentHike } from '../redux/selectors';
|
||||
} from '../../redux/actions';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
|
||||
const answerThreshold = 100;
|
||||
const springProperties = { stiffness: 120, damping: 10 };
|
||||
|
@ -22,34 +22,30 @@ const actionsToBind = {
|
|||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
getCurrentHike,
|
||||
state => state.hikesApp,
|
||||
challengeSelector,
|
||||
state => state.challengesApp,
|
||||
state => state.app.isSignedIn,
|
||||
(currentHike, ui, isSignedIn) => {
|
||||
const {
|
||||
(
|
||||
{ challenge: { tests = [ ] }},
|
||||
{
|
||||
currentQuestion = 1,
|
||||
mouse = [ 0, 0 ],
|
||||
delta = [ 0, 0 ],
|
||||
isCorrect = false,
|
||||
isPressed = false,
|
||||
shouldShakeQuestion = false
|
||||
} = ui;
|
||||
|
||||
const {
|
||||
tests = []
|
||||
} = currentHike;
|
||||
|
||||
return {
|
||||
tests,
|
||||
currentQuestion,
|
||||
isCorrect,
|
||||
mouse,
|
||||
delta,
|
||||
isPressed,
|
||||
shouldShakeQuestion,
|
||||
isSignedIn
|
||||
};
|
||||
}
|
||||
},
|
||||
isSignedIn
|
||||
) => ({
|
||||
tests,
|
||||
currentQuestion,
|
||||
isCorrect,
|
||||
mouse,
|
||||
delta,
|
||||
isPressed,
|
||||
shouldShakeQuestion,
|
||||
isSignedIn
|
||||
})
|
||||
);
|
||||
|
||||
class Question extends React.Component {
|
||||
|
@ -133,7 +129,8 @@ class Question extends React.Component {
|
|||
onTouchEnd={ mouseUp }
|
||||
onTouchMove={ this.handleMouseMove(isPressed, this.props) }
|
||||
onTouchStart={ grabQuestion }
|
||||
style={ style }>
|
||||
style={ style }
|
||||
>
|
||||
<h4>Question { number }</h4>
|
||||
<p>{ question }</p>
|
||||
</article>
|
||||
|
@ -162,7 +159,8 @@ class Question extends React.Component {
|
|||
<Col
|
||||
onMouseUp={ e => this.handleMouseUp(e, answer, info) }
|
||||
xs={ 8 }
|
||||
xsOffset={ 2 }>
|
||||
xsOffset={ 2 }
|
||||
>
|
||||
<Row>
|
||||
<Motion style={{ x: spring(xPosition, springProperties) }}>
|
||||
{ questionElement }
|
||||
|
@ -174,14 +172,16 @@ class Question extends React.Component {
|
|||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
className='pull-left'
|
||||
onClick={ this.onAnswer(answer, false, info) }>
|
||||
onClick={ this.onAnswer(answer, false, info) }
|
||||
>
|
||||
false
|
||||
</Button>
|
||||
<Button
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
className='pull-right'
|
||||
onClick={ this.onAnswer(answer, true, info) }>
|
||||
onClick={ this.onAnswer(answer, true, info) }
|
||||
>
|
||||
true
|
||||
</Button>
|
||||
</div>
|
|
@ -5,26 +5,27 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import Lecture from './Lecture.jsx';
|
||||
import Questions from './Questions.jsx';
|
||||
import { resetHike } from '../redux/actions';
|
||||
import { updateTitle } from '../../../redux/actions';
|
||||
import { getCurrentHike } from '../redux/selectors';
|
||||
import { resetUi } from '../../redux/actions';
|
||||
import { updateTitle } from '../../../../redux/actions';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
|
||||
const bindableActions = { resetUi, updateTitle };
|
||||
const mapStateToProps = createSelector(
|
||||
getCurrentHike,
|
||||
state => state.hikesApp.shouldShowQuestions,
|
||||
(currentHike, shouldShowQuestions) => ({
|
||||
title: currentHike ? currentHike.title : '',
|
||||
challengeSelector,
|
||||
state => state.challengesApp.shouldShowQuestions,
|
||||
({ challenge: { title } }, shouldShowQuestions) => ({
|
||||
title,
|
||||
shouldShowQuestions
|
||||
})
|
||||
);
|
||||
|
||||
// export plain component for testing
|
||||
export class Hike extends React.Component {
|
||||
static displayName = 'Hike';
|
||||
export class Video extends React.Component {
|
||||
static displayName = 'Video';
|
||||
|
||||
static propTypes = {
|
||||
// actions
|
||||
resetHike: PropTypes.func,
|
||||
resetUi: PropTypes.func,
|
||||
// ui
|
||||
title: PropTypes.string,
|
||||
params: PropTypes.object,
|
||||
|
@ -38,12 +39,12 @@ export class Hike extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetHike();
|
||||
this.props.resetUi();
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ params: { dashedName } }) {
|
||||
if (this.props.params.dashedName !== dashedName) {
|
||||
this.props.resetHike();
|
||||
componentWillReceiveProps({ title }) {
|
||||
if (this.props.title !== title) {
|
||||
this.props.resetUi();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +70,8 @@ export class Hike extends React.Component {
|
|||
<div className='spacer' />
|
||||
<section
|
||||
className={ 'text-center' }
|
||||
title={ title }>
|
||||
title={ title }
|
||||
>
|
||||
{ this.renderBody(shouldShowQuestions) }
|
||||
</section>
|
||||
</Col>
|
||||
|
@ -78,4 +80,7 @@ export class Hike extends React.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 { updateContents } from '../../../../utils/polyvinyl';
|
||||
import { loggerToStr } from '../utils';
|
||||
import { getMouse, loggerToStr } from '../utils';
|
||||
|
||||
import types from './types';
|
||||
|
||||
|
@ -79,3 +79,40 @@ export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
|||
export const saveCode = createAction(types.saveCode);
|
||||
export const loadCode = createAction(types.loadCode);
|
||||
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);
|
||||
|
|
|
@ -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 { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
|
||||
// NOTE(@BerkeleyTrue): this file could benefit from some refactoring.
|
||||
// lots of repeat code
|
||||
|
||||
function completedChallenge(state) {
|
||||
let body;
|
||||
let isSignedIn = false;
|
||||
|
@ -163,6 +166,7 @@ function submitSimpleChallenge(type, state) {
|
|||
const submitTypes = {
|
||||
tests: submitModern,
|
||||
step: submitSimpleChallenge,
|
||||
video: submitSimpleChallenge,
|
||||
'project.frontEnd': submitProject,
|
||||
'project.backEnd': submitProject,
|
||||
'project.simple': submitSimpleChallenge
|
||||
|
|
|
@ -5,11 +5,13 @@ export types from './types';
|
|||
import fetchChallengesSaga from './fetch-challenges-saga';
|
||||
import completionSaga from './completion-saga';
|
||||
import nextChallengeSaga from './next-challenge-saga';
|
||||
import answerSaga from './answer-saga';
|
||||
|
||||
export projectNormalizer from './project-normalizer';
|
||||
|
||||
export const sagas = [
|
||||
fetchChallengesSaga,
|
||||
completionSaga,
|
||||
nextChallengeSaga
|
||||
nextChallengeSaga,
|
||||
answerSaga
|
||||
];
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
getFirstChallengeOfNextBlock,
|
||||
getFirstChallengeOfNextSuperBlock
|
||||
} from '../utils';
|
||||
import { getRandomVerb } from '../../../utils/get-words';
|
||||
import { randomVerb } from '../../../utils/get-words';
|
||||
|
||||
export default function nextChallengeSaga(actions$, getState) {
|
||||
return actions$
|
||||
|
@ -48,7 +48,7 @@ export default function nextChallengeSaga(actions$, getState) {
|
|||
}
|
||||
message += ' Your next challenge has arrived.';
|
||||
const toast = {
|
||||
// title: isNewSuperBlock || isNewBlock ? getRandomVerb() : null,
|
||||
// title: isNewSuperBlock || isNewBlock ? randomVerb() : null,
|
||||
message
|
||||
};
|
||||
*/
|
||||
|
@ -56,7 +56,7 @@ export default function nextChallengeSaga(actions$, getState) {
|
|||
updateCurrentChallenge(nextChallenge),
|
||||
resetUi(),
|
||||
makeToast({
|
||||
title: getRandomVerb(),
|
||||
title: randomVerb(),
|
||||
message: 'Your next challenge has arrived.'
|
||||
}),
|
||||
push(`/challenges/${nextChallenge.block}/${nextChallenge.dashedName}`)
|
||||
|
|
|
@ -12,15 +12,30 @@ import {
|
|||
} from '../utils';
|
||||
|
||||
const initialUiState = {
|
||||
// step index tracing
|
||||
currentIndex: 0,
|
||||
previousIndex: -1,
|
||||
// step action
|
||||
isActionCompleted: false,
|
||||
isSubmitting: true,
|
||||
// project is ready to submit
|
||||
isSubmitting: false,
|
||||
output: `/**
|
||||
* Any console.log()
|
||||
* statements will appear in
|
||||
* 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 = {
|
||||
id: '',
|
||||
|
@ -107,6 +122,49 @@ const mainReducer = handleActions(
|
|||
[types.updateOutput]: (state, { payload: output }) => ({
|
||||
...state,
|
||||
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
|
||||
|
|
|
@ -39,5 +39,21 @@ export default createTypes([
|
|||
// code storage
|
||||
'saveCode',
|
||||
'loadCode',
|
||||
'savedCodeFound'
|
||||
'savedCodeFound',
|
||||
|
||||
// video challenges
|
||||
'toggleQuestionView',
|
||||
'grabQuestion',
|
||||
'releaseQuestion',
|
||||
'moveQuestion',
|
||||
|
||||
'answerQuestion',
|
||||
|
||||
'startShake',
|
||||
'endShake',
|
||||
|
||||
'primeNextQuestion',
|
||||
'goToNextQuestion',
|
||||
'transitionVideo',
|
||||
'videoCompleted'
|
||||
], 'challenges');
|
||||
|
|
|
@ -168,3 +168,21 @@ export function getCurrentSuperBlockName(current, entities) {
|
|||
const block = blockMap[challenge.block];
|
||||
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 NotFound from '../components/NotFound/index.jsx';
|
||||
|
||||
export default {
|
||||
path: '/',
|
||||
childRoutes: [
|
||||
Jobs,
|
||||
Hikes,
|
||||
challenges,
|
||||
modernChallenges,
|
||||
map,
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
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';
|
||||
|
||||
export default [
|
||||
...appSagas,
|
||||
...hikesSagas,
|
||||
...jobsSagas,
|
||||
...challengeSagas
|
||||
];
|
||||
|
|
Loading…
Reference in New Issue