From 4a043e151e169173383d4b6ba2fdb53c7509b9f0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 13 Jun 2016 12:26:30 -0700 Subject: [PATCH] Move Video challenges under challenges dir Remove old hikes components Remove unused jobs stuff --- client/sagas/index.js | 2 - client/sagas/local-storage-saga.js | 43 --- common/app/create-reducer.js | 12 +- common/app/routes/Hikes/components/Hikes.jsx | 71 ---- .../app/routes/Hikes/components/Lecture.jsx | 102 ----- common/app/routes/Hikes/components/Map.jsx | 39 -- common/app/routes/Hikes/index.js | 24 -- common/app/routes/Hikes/redux/actions.js | 58 --- common/app/routes/Hikes/redux/answer-saga.js | 138 ------- .../routes/Hikes/redux/fetch-hikes-saga.js | 28 -- common/app/routes/Hikes/redux/index.js | 8 - common/app/routes/Hikes/redux/reducer.js | 99 ----- common/app/routes/Hikes/redux/selectors.js | 8 - common/app/routes/Hikes/redux/types.js | 24 -- common/app/routes/Hikes/redux/utils.js | 76 ---- common/app/routes/Jobs/README.md | 1 - .../routes/Jobs/components/JobNotFound.jsx | 35 -- .../app/routes/Jobs/components/JobTotal.jsx | 306 --------------- common/app/routes/Jobs/components/Jobs.jsx | 149 -------- common/app/routes/Jobs/components/List.jsx | 86 ----- common/app/routes/Jobs/components/NewJob.jsx | 361 ------------------ .../Jobs/components/NewJobCompleted.jsx | 43 --- common/app/routes/Jobs/components/Preview.jsx | 94 ----- common/app/routes/Jobs/components/Show.jsx | 146 ------- common/app/routes/Jobs/components/ShowJob.jsx | 147 ------- common/app/routes/Jobs/index.js | 36 -- common/app/routes/Jobs/redux/actions.js | 35 -- .../app/routes/Jobs/redux/apply-promo-saga.js | 33 -- .../app/routes/Jobs/redux/fetch-jobs-saga.js | 36 -- common/app/routes/Jobs/redux/index.js | 11 - .../routes/Jobs/redux/jobs-form-normalizer.js | 19 - common/app/routes/Jobs/redux/reducer.js | 79 ---- common/app/routes/Jobs/redux/save-job-saga.js | 25 -- common/app/routes/Jobs/redux/types.js | 22 -- common/app/routes/Jobs/utils.js | 29 -- .../app/routes/challenges/components/Show.jsx | 4 +- .../challenges/components/video/Lecture.jsx | 110 ++++++ .../components/video}/Questions.jsx | 54 +-- .../components/video/Video.jsx} | 37 +- common/app/routes/challenges/redux/actions.js | 39 +- .../routes/challenges/redux/answer-saga.js | 86 +++++ .../challenges/redux/completion-saga.js | 4 + common/app/routes/challenges/redux/index.js | 4 +- .../challenges/redux/next-challenge-saga.js | 6 +- common/app/routes/challenges/redux/reducer.js | 62 ++- common/app/routes/challenges/redux/types.js | 18 +- common/app/routes/challenges/utils.js | 18 + common/app/routes/index.js | 4 - common/app/sagas.js | 4 - 49 files changed, 391 insertions(+), 2484 deletions(-) delete mode 100644 client/sagas/local-storage-saga.js delete mode 100644 common/app/routes/Hikes/components/Hikes.jsx delete mode 100644 common/app/routes/Hikes/components/Lecture.jsx delete mode 100644 common/app/routes/Hikes/components/Map.jsx delete mode 100644 common/app/routes/Hikes/index.js delete mode 100644 common/app/routes/Hikes/redux/actions.js delete mode 100644 common/app/routes/Hikes/redux/answer-saga.js delete mode 100644 common/app/routes/Hikes/redux/fetch-hikes-saga.js delete mode 100644 common/app/routes/Hikes/redux/index.js delete mode 100644 common/app/routes/Hikes/redux/reducer.js delete mode 100644 common/app/routes/Hikes/redux/selectors.js delete mode 100644 common/app/routes/Hikes/redux/types.js delete mode 100644 common/app/routes/Hikes/redux/utils.js delete mode 100644 common/app/routes/Jobs/README.md delete mode 100644 common/app/routes/Jobs/components/JobNotFound.jsx delete mode 100644 common/app/routes/Jobs/components/JobTotal.jsx delete mode 100644 common/app/routes/Jobs/components/Jobs.jsx delete mode 100644 common/app/routes/Jobs/components/List.jsx delete mode 100644 common/app/routes/Jobs/components/NewJob.jsx delete mode 100644 common/app/routes/Jobs/components/NewJobCompleted.jsx delete mode 100644 common/app/routes/Jobs/components/Preview.jsx delete mode 100644 common/app/routes/Jobs/components/Show.jsx delete mode 100644 common/app/routes/Jobs/components/ShowJob.jsx delete mode 100644 common/app/routes/Jobs/index.js delete mode 100644 common/app/routes/Jobs/redux/actions.js delete mode 100644 common/app/routes/Jobs/redux/apply-promo-saga.js delete mode 100644 common/app/routes/Jobs/redux/fetch-jobs-saga.js delete mode 100644 common/app/routes/Jobs/redux/index.js delete mode 100644 common/app/routes/Jobs/redux/jobs-form-normalizer.js delete mode 100644 common/app/routes/Jobs/redux/reducer.js delete mode 100644 common/app/routes/Jobs/redux/save-job-saga.js delete mode 100644 common/app/routes/Jobs/redux/types.js delete mode 100644 common/app/routes/Jobs/utils.js create mode 100644 common/app/routes/challenges/components/video/Lecture.jsx rename common/app/routes/{Hikes/components => challenges/components/video}/Questions.jsx (87%) rename common/app/routes/{Hikes/components/Hike.jsx => challenges/components/video/Video.jsx} (64%) create mode 100644 common/app/routes/challenges/redux/answer-saga.js diff --git a/client/sagas/index.js b/client/sagas/index.js index 24a84a73e69..54cd65a21b1 100644 --- a/client/sagas/index.js +++ b/client/sagas/index.js @@ -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, diff --git a/client/sagas/local-storage-saga.js b/client/sagas/local-storage-saga.js deleted file mode 100644 index d6e60dc20ea..00000000000 --- a/client/sagas/local-storage-saga.js +++ /dev/null @@ -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)); - }); -} diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js index e91c704a8e4..f7edc27f932 100644 --- a/common/app/create-reducer.js +++ b/common/app/create-reducer.js @@ -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 }) }); } diff --git a/common/app/routes/Hikes/components/Hikes.jsx b/common/app/routes/Hikes/components/Hikes.jsx deleted file mode 100644 index ccc7a68df06..00000000000 --- a/common/app/routes/Hikes/components/Hikes.jsx +++ /dev/null @@ -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 ( - - ); - } - - render() { - const { hikes } = this.props; - return ( -
- { - // render sub-route - this.props.children || - // if no sub-route render hikes map - this.renderMap(hikes) - } -
- ); - } -} - -// export redux and fetch aware component -export default compose( - connect(mapStateToProps, { fetchHikes }), - contain(fetchOptions) -)(Hikes); diff --git a/common/app/routes/Hikes/components/Lecture.jsx b/common/app/routes/Hikes/components/Lecture.jsx deleted file mode 100644 index e6d85643eda..00000000000 --- a/common/app/routes/Hikes/components/Lecture.jsx +++ /dev/null @@ -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) => ( -

- )); - } - - render() { - const { - id = '1', - description = [], - toggleQuestionView - } = this.props; - - const dashedName = 'foo'; - - return ( - - - - - -

- { this.renderTranscript(description, dashedName) } -
- - - - ); - } -} - -export default connect( - mapStateToProps, - { hardGoTo, toggleQuestionView } -)(Lecture); diff --git a/common/app/routes/Hikes/components/Map.jsx b/common/app/routes/Hikes/components/Map.jsx deleted file mode 100644 index 391c68693b3..00000000000 --- a/common/app/routes/Hikes/components/Map.jsx +++ /dev/null @@ -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 ( - - -

{ title }

- -
- ); - }); - - return ( -
-
-

Welcome To Hikes!

-
-
- - { vidElements } - -
- ); - } -}); diff --git a/common/app/routes/Hikes/index.js b/common/app/routes/Hikes/index.js deleted file mode 100644 index 206bf97b4d9..00000000000 --- a/common/app/routes/Hikes/index.js +++ /dev/null @@ -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' - ); - } -}; diff --git a/common/app/routes/Hikes/redux/actions.js b/common/app/routes/Hikes/redux/actions.js deleted file mode 100644 index 10d434f0687..00000000000 --- a/common/app/routes/Hikes/redux/actions.js +++ /dev/null @@ -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); diff --git a/common/app/routes/Hikes/redux/answer-saga.js b/common/app/routes/Hikes/redux/answer-saga.js deleted file mode 100644 index 3b2ee5ad6a2..00000000000 --- a/common/app/routes/Hikes/redux/answer-saga.js +++ /dev/null @@ -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')); - }); -} diff --git a/common/app/routes/Hikes/redux/fetch-hikes-saga.js b/common/app/routes/Hikes/redux/fetch-hikes-saga.js deleted file mode 100644 index 748f8c29619..00000000000 --- a/common/app/routes/Hikes/redux/fetch-hikes-saga.js +++ /dev/null @@ -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); - }); -} diff --git a/common/app/routes/Hikes/redux/index.js b/common/app/routes/Hikes/redux/index.js deleted file mode 100644 index 8d94299b341..00000000000 --- a/common/app/routes/Hikes/redux/index.js +++ /dev/null @@ -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 ]; diff --git a/common/app/routes/Hikes/redux/reducer.js b/common/app/routes/Hikes/redux/reducer.js deleted file mode 100644 index 5a850643c62..00000000000 --- a/common/app/routes/Hikes/redux/reducer.js +++ /dev/null @@ -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 -); diff --git a/common/app/routes/Hikes/redux/selectors.js b/common/app/routes/Hikes/redux/selectors.js deleted file mode 100644 index 507a90e1c22..00000000000 --- a/common/app/routes/Hikes/redux/selectors.js +++ /dev/null @@ -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] || {}) -); diff --git a/common/app/routes/Hikes/redux/types.js b/common/app/routes/Hikes/redux/types.js deleted file mode 100644 index 26e547dce06..00000000000 --- a/common/app/routes/Hikes/redux/types.js +++ /dev/null @@ -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'); diff --git a/common/app/routes/Hikes/redux/utils.js b/common/app/routes/Hikes/redux/utils.js deleted file mode 100644 index 6b3f6f0fbe6..00000000000 --- a/common/app/routes/Hikes/redux/utils.js +++ /dev/null @@ -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]; -} diff --git a/common/app/routes/Jobs/README.md b/common/app/routes/Jobs/README.md deleted file mode 100644 index 5dde417dab4..00000000000 --- a/common/app/routes/Jobs/README.md +++ /dev/null @@ -1 +0,0 @@ -This folder contains everything relative to Jobs board diff --git a/common/app/routes/Jobs/components/JobNotFound.jsx b/common/app/routes/Jobs/components/JobNotFound.jsx deleted file mode 100644 index e05a8866946..00000000000 --- a/common/app/routes/Jobs/components/JobNotFound.jsx +++ /dev/null @@ -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 ( -
- - -
- No job found... - - - -
- -
-
- ); - } -} diff --git a/common/app/routes/Jobs/components/JobTotal.jsx b/common/app/routes/Jobs/components/JobTotal.jsx deleted file mode 100644 index df540408db8..00000000000 --- a/common/app/routes/Jobs/components/JobTotal.jsx +++ /dev/null @@ -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 ( - - -

Promo Discount

- - -

-{ discountAmount }

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

Highlighting

- - -

+ 250

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

- One more step -

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

Job Posting

- - -

+ { price }

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

Total

- - -

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

- -
-
- { this.renderPromo() } -
- - -
setTimeout(push, 0, '/jobs') } - target='_blank'> - - - - -
- An array of credit cards - - - -
-
- - -
- ); - } -} - -export default connect(mapStateToProps, bindableActions)(JobTotal); diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx deleted file mode 100644 index d0a7102fe8c..00000000000 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ /dev/null @@ -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 ( - - ); - } - - renderChild(child, jobs) { - if (!child) { - return null; - } - return cloneElement( - child, - { jobs } - ); - } - - render() { - const { - children, - jobs - } = this.props; - - return ( - - -

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

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

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

-
- Michael Gai, CEO at CoNarrative -
-
- -
- - { this.renderChild(children, jobs) || - this.renderList(this.createJobClickHandler(), jobs) } - - - - ); - } -} - -export default compose( - connect(mapStateToProps, bindableActions), - contain(fetchOptions) -)(Jobs); diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx deleted file mode 100644 index daebd1d999a..00000000000 --- a/common/app/routes/Jobs/components/List.jsx +++ /dev/null @@ -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 ( - - { locale } - - ); - } - - 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 ( - - handleClick(id) }> -
-

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

-

- { this.addLocation(locale) } -

-
-
-
- ); - }); - } - - render() { - const { - handleClick, - jobs - } = this.props; - - return ( - - { this.renderJobs(handleClick, jobs) } - - ); - } -} diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx deleted file mode 100644 index 1bab05eb77c..00000000000 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ /dev/null @@ -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 ( -
- - -
-
this.handleSubmit(data)) }> - -
-

First, select your ideal applicant:

-
- - - - - - -
- - - - - -
-

Tell us about the position

-
-
- - - - -
- -
- -
-

How should they apply?

-
- -
- -
-
-
-

Tell us about your organization

-
- handleChange('company', e) } - type='text' - wrapperClassName={ inputClass } - { ...company } - /> - - - - -
-
-
-
-

Make it stand out

-
-
- - - Highlight this ad to give it extra attention. -
- Featured listings receive more clicks and more applications. - -
-
- - - -
- - - - - - - -
- - -
- ); - } -} - -export default reduxForm( - { - form: 'NewJob', - fields, - validate: createFormValidator(fieldValidators) - }, - state => ({ initialValues: state.jobsApp.initialValues }), - { - loadSavedForm, - push, - saveForm - } -)(NewJob); diff --git a/common/app/routes/Jobs/components/NewJobCompleted.jsx b/common/app/routes/Jobs/components/NewJobCompleted.jsx deleted file mode 100644 index 7d401998443..00000000000 --- a/common/app/routes/Jobs/components/NewJobCompleted.jsx +++ /dev/null @@ -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 ( -
-
- -

- Your Position has Been Submitted -

-
- - - We’ll review your listing and email you when it’s live. -
- Thank you for listing this job with Free Code Camp. - -
-
- - - -
-
- ); - } -} diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx deleted file mode 100644 index 7509408e503..00000000000 --- a/common/app/routes/Jobs/components/Preview.jsx +++ /dev/null @@ -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 ; - } - - return ( -
- -
-
- - -
- - -
- -
-
- ); - } -} - -export default connect(mapStateToProps, bindableActions)(JobPreview); diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx deleted file mode 100644 index 5a1f9fa3cf6..00000000000 --- a/common/app/routes/Jobs/components/Show.jsx +++ /dev/null @@ -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 ; - } - - const isSignedIn = !!username; - - const showApply = shouldShowApply( - job, - { isFrontEndCert, isBackEndCert } - ); - - const message = generateMessage( - job, - { isFrontEndCert, isBackEndCert, isSignedIn } - ); - - return ( - - ); - } -} - -export default compose( - connect(mapStateToProps, bindableActions), - contain(fetchOptions) -)(Show); diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx deleted file mode 100644 index b6bdc2030c8..00000000000 --- a/common/app/routes/Jobs/components/ShowJob.jsx +++ /dev/null @@ -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 `${match}`; - }); -} - -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 ( -
-

{ company }

-
- { position } -
-
- ); - } - - renderHowToApply(showApply, preview, message, howToApply) { - if (!showApply) { - return ( - - -

{ message }

- -
- ); - } - - return ( - -
- -
- { preview ? 'How do I apply?' : message } -
-
- -
- -
- ); - } - - render() { - const { - showApply = true, - message, - preview = true, - job = {} - } = this.props; - - const { - logo, - position, - city, - company, - state, - locale, - description, - howToApply - } = job; - - return ( -
- - -
- -

- { company } -

-
-
- - - - - - - Position: { position || 'N/A' } -
- Location: - { locale ? locale : `${city}, ${state}` } - -
-
-
- - -

{ description }

- -
- { this.renderHowToApply(showApply, preview, message, howToApply) } -
- - -
- ); - } -} diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js deleted file mode 100644 index 1c7dfa85eda..00000000000 --- a/common/app/routes/Jobs/index.js +++ /dev/null @@ -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' - ); - } -}; diff --git a/common/app/routes/Jobs/redux/actions.js b/common/app/routes/Jobs/redux/actions.js deleted file mode 100644 index 40a328f7574..00000000000 --- a/common/app/routes/Jobs/redux/actions.js +++ /dev/null @@ -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); diff --git a/common/app/routes/Jobs/redux/apply-promo-saga.js b/common/app/routes/Jobs/redux/apply-promo-saga.js deleted file mode 100644 index 102dff7bcc3..00000000000 --- a/common/app/routes/Jobs/redux/apply-promo-saga.js +++ /dev/null @@ -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 - })); - }); -} diff --git a/common/app/routes/Jobs/redux/fetch-jobs-saga.js b/common/app/routes/Jobs/redux/fetch-jobs-saga.js deleted file mode 100644 index 1d8c4244dcf..00000000000 --- a/common/app/routes/Jobs/redux/fetch-jobs-saga.js +++ /dev/null @@ -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 })); - }); -} diff --git a/common/app/routes/Jobs/redux/index.js b/common/app/routes/Jobs/redux/index.js deleted file mode 100644 index fe3f432b3e8..00000000000 --- a/common/app/routes/Jobs/redux/index.js +++ /dev/null @@ -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 ]; diff --git a/common/app/routes/Jobs/redux/jobs-form-normalizer.js b/common/app/routes/Jobs/redux/jobs-form-normalizer.js deleted file mode 100644 index df7baa8507a..00000000000 --- a/common/app/routes/Jobs/redux/jobs-form-normalizer.js +++ /dev/null @@ -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) - } -}; diff --git a/common/app/routes/Jobs/redux/reducer.js b/common/app/routes/Jobs/redux/reducer.js deleted file mode 100644 index 2a270b777b5..00000000000 --- a/common/app/routes/Jobs/redux/reducer.js +++ /dev/null @@ -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 -); diff --git a/common/app/routes/Jobs/redux/save-job-saga.js b/common/app/routes/Jobs/redux/save-job-saga.js deleted file mode 100644 index f9158b12455..00000000000 --- a/common/app/routes/Jobs/redux/save-job-saga.js +++ /dev/null @@ -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 - })); - }); -} diff --git a/common/app/routes/Jobs/redux/types.js b/common/app/routes/Jobs/redux/types.js deleted file mode 100644 index 9a387f58c73..00000000000 --- a/common/app/routes/Jobs/redux/types.js +++ /dev/null @@ -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'); diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js deleted file mode 100644 index 6a8f1705b74..00000000000 --- a/common/app/routes/Jobs/utils.js +++ /dev/null @@ -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; -} diff --git a/common/app/routes/challenges/components/Show.jsx b/common/app/routes/challenges/components/Show.jsx index badc1f94778..07fb075d6ab 100644 --- a/common/app/routes/challenges/components/Show.jsx +++ b/common/app/routes/challenges/components/Show.jsx @@ -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 = { diff --git a/common/app/routes/challenges/components/video/Lecture.jsx b/common/app/routes/challenges/components/video/Lecture.jsx new file mode 100644 index 00000000000..0f4cd42970a --- /dev/null +++ b/common/app/routes/challenges/components/video/Lecture.jsx @@ -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) => ( +

+ )); + } + + render() { + const { + id, + videoId, + description = [], + toggleQuestionView + } = this.props; + + const dashedName = 'foo'; + + return ( + + +

+ +
+ + + +
+ { this.renderTranscript(description, dashedName) } +
+ +
+ + + + ); + } +} + +export default connect( + mapStateToProps, + { toggleQuestionView } +)(Lecture); diff --git a/common/app/routes/Hikes/components/Questions.jsx b/common/app/routes/challenges/components/video/Questions.jsx similarity index 87% rename from common/app/routes/Hikes/components/Questions.jsx rename to common/app/routes/challenges/components/video/Questions.jsx index 55911b6d028..e753b7fbe13 100644 --- a/common/app/routes/Hikes/components/Questions.jsx +++ b/common/app/routes/challenges/components/video/Questions.jsx @@ -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 } + >

Question { number }

{ question }

@@ -162,7 +159,8 @@ class Question extends React.Component { this.handleMouseUp(e, answer, info) } xs={ 8 } - xsOffset={ 2 }> + xsOffset={ 2 } + > { 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
diff --git a/common/app/routes/Hikes/components/Hike.jsx b/common/app/routes/challenges/components/video/Video.jsx similarity index 64% rename from common/app/routes/Hikes/components/Hike.jsx rename to common/app/routes/challenges/components/video/Video.jsx index ed42875864c..3937ca0b402 100644 --- a/common/app/routes/Hikes/components/Hike.jsx +++ b/common/app/routes/challenges/components/video/Video.jsx @@ -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 {
+ title={ title } + > { this.renderBody(shouldShowQuestions) }
@@ -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); diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js index bae870b397b..de904ac50dc 100644 --- a/common/app/routes/challenges/redux/actions.js +++ b/common/app/routes/challenges/redux/actions.js @@ -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); diff --git a/common/app/routes/challenges/redux/answer-saga.js b/common/app/routes/challenges/redux/answer-saga.js new file mode 100644 index 00000000000..b9ccedf028c --- /dev/null +++ b/common/app/routes/challenges/redux/answer-saga.js @@ -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); + }); +} diff --git a/common/app/routes/challenges/redux/completion-saga.js b/common/app/routes/challenges/redux/completion-saga.js index 502548c0c25..870698b66a2 100644 --- a/common/app/routes/challenges/redux/completion-saga.js +++ b/common/app/routes/challenges/redux/completion-saga.js @@ -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 diff --git a/common/app/routes/challenges/redux/index.js b/common/app/routes/challenges/redux/index.js index a7dec455004..5d0fc7cc672 100644 --- a/common/app/routes/challenges/redux/index.js +++ b/common/app/routes/challenges/redux/index.js @@ -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 ]; diff --git a/common/app/routes/challenges/redux/next-challenge-saga.js b/common/app/routes/challenges/redux/next-challenge-saga.js index 06a15c11d87..bb2daacd9be 100644 --- a/common/app/routes/challenges/redux/next-challenge-saga.js +++ b/common/app/routes/challenges/redux/next-challenge-saga.js @@ -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}`) diff --git a/common/app/routes/challenges/redux/reducer.js b/common/app/routes/challenges/redux/reducer.js index f224ec98013..c56b4828ef7 100644 --- a/common/app/routes/challenges/redux/reducer.js +++ b/common/app/routes/challenges/redux/reducer.js @@ -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 diff --git a/common/app/routes/challenges/redux/types.js b/common/app/routes/challenges/redux/types.js index b29c85d2973..87e34beadf4 100644 --- a/common/app/routes/challenges/redux/types.js +++ b/common/app/routes/challenges/redux/types.js @@ -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'); diff --git a/common/app/routes/challenges/utils.js b/common/app/routes/challenges/utils.js index dff8cfffdd7..12fab290e6d 100644 --- a/common/app/routes/challenges/utils.js +++ b/common/app/routes/challenges/utils.js @@ -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]; +} diff --git a/common/app/routes/index.js b/common/app/routes/index.js index bbb70e7438a..b2ea7c01550 100644 --- a/common/app/routes/index.js +++ b/common/app/routes/index.js @@ -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, diff --git a/common/app/sagas.js b/common/app/sagas.js index 18e4f31589e..3b668202f9c 100644 --- a/common/app/sagas.js +++ b/common/app/sagas.js @@ -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 ];