From acf4d99f67fb6fbc5a037ab9cce8c59bea38d519 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 9 Jun 2016 16:02:51 -0700 Subject: [PATCH] Add block scoping to challenges url --- common/app/redux/actions.js | 2 + common/app/redux/reducer.js | 4 ++ common/app/redux/types.js | 1 + .../app/routes/challenges/components/Show.jsx | 4 +- .../challenges/components/map/Block.jsx | 29 ++++++++--- .../challenges/components/map/Super-Block.jsx | 9 ++-- common/app/routes/challenges/index.js | 5 ++ common/app/routes/challenges/redux/actions.js | 5 +- .../challenges/redux/completion-saga.js | 23 +-------- .../challenges/redux/fetch-challenges-saga.js | 14 ++++-- .../challenges/redux/next-challenge-saga.js | 23 +++++++++ common/app/routes/challenges/utils.js | 2 +- common/app/routes/index.js | 3 +- server/boot/a-react.js | 11 ++++- server/services/map.js | 49 ++++++++++++++++++- 15 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 common/app/routes/challenges/redux/next-challenge-saga.js diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js index abd323f8eb..2b6f45f3b0 100644 --- a/common/app/redux/actions.js +++ b/common/app/redux/actions.js @@ -28,6 +28,8 @@ export const setUser = createAction(types.setUser); // updatePoints(points: Number) => Action export const updatePoints = createAction(types.updatePoints); +// used when server needs client to redirect +export const delayedRedirect = createAction(types.delayedRedirect); // hardGoTo(path: String) => Action export const hardGoTo = createAction(types.hardGoTo); diff --git a/common/app/redux/reducer.js b/common/app/redux/reducer.js index ee93ba151d..d3d28812e3 100644 --- a/common/app/redux/reducer.js +++ b/common/app/redux/reducer.js @@ -55,6 +55,10 @@ export default handleActions( [types.toggleMainChat]: state => ({ ...state, isMainChatOpen: !state.isMainChatOpen + }), + [types.delayedRedirect]: (state, { payload }) => ({ + ...state, + delayedRedirect: payload }) }, initialState diff --git a/common/app/redux/types.js b/common/app/redux/types.js index 28652d66ba..d9914ce825 100644 --- a/common/app/redux/types.js +++ b/common/app/redux/types.js @@ -11,6 +11,7 @@ export default createTypes([ 'handleError', // used to hit the server 'hardGoTo', + 'delayedRedirect', 'initWindowHeight', 'updateWindowHeight', diff --git a/common/app/routes/challenges/components/Show.jsx b/common/app/routes/challenges/components/Show.jsx index 095af60b0e..badc1f9477 100644 --- a/common/app/routes/challenges/components/Show.jsx +++ b/common/app/routes/challenges/components/Show.jsx @@ -35,8 +35,8 @@ const mapStateToProps = createSelector( const fetchOptions = { fetchAction: 'fetchChallenge', - getActionArgs({ params: { dashedName } }) { - return [ dashedName ]; + getActionArgs({ params: { block, dashedName } }) { + return [ dashedName, block ]; }, isPrimed({ challenge }) { return !!challenge; diff --git a/common/app/routes/challenges/components/map/Block.jsx b/common/app/routes/challenges/components/map/Block.jsx index 1d1bfddf94..8b807a36ae 100644 --- a/common/app/routes/challenges/components/map/Block.jsx +++ b/common/app/routes/challenges/components/map/Block.jsx @@ -13,12 +13,13 @@ export class Block extends PureComponent { static displayName = 'Block'; static propTypes = { title: PropTypes.string, + dashedName: PropTypes.string, time: PropTypes.string, challenges: PropTypes.array, updateCurrentChallenge: PropTypes.func }; - renderChallenges(challenges, updateCurrentChallenge) { + renderChallenges(blockName, challenges, updateCurrentChallenge) { if (!Array.isArray(challenges) || !challenges.length) { return
No Challenges Found
; } @@ -37,7 +38,8 @@ export class Block extends PureComponent { return (

+ key={ title } + > { title } { isRequired ? @@ -50,10 +52,12 @@ export class Block extends PureComponent { return (

- + key={ title } + > + updateCurrentChallenge(challenge) }> + onClick={ () => updateCurrentChallenge(challenge) } + > { title } complete { @@ -69,7 +73,13 @@ export class Block extends PureComponent { } render() { - const { title, time, challenges, updateCurrentChallenge } = this.props; + const { + title, + time, + challenges, + updateCurrentChallenge, + dashedName + } = this.props; return ( } id={ title } - key={ title }> - { this.renderChallenges(challenges, updateCurrentChallenge) } + key={ title } + > + { + this.renderChallenges(dashedName, challenges, updateCurrentChallenge) + } ); } diff --git a/common/app/routes/challenges/components/map/Super-Block.jsx b/common/app/routes/challenges/components/map/Super-Block.jsx index 323568de44..a8aa048066 100644 --- a/common/app/routes/challenges/components/map/Super-Block.jsx +++ b/common/app/routes/challenges/components/map/Super-Block.jsx @@ -21,7 +21,8 @@ export default class SuperBlock extends PureComponent { return ( + { ...block } + /> ); }); } @@ -35,7 +36,8 @@ export default class SuperBlock extends PureComponent { expanded={ true } header={

{ title }

} id={ title } - key={ title }> + key={ title } + > { message ?
@@ -44,7 +46,8 @@ export default class SuperBlock extends PureComponent { '' }
+ className='map-accordion-block' + > { this.renderBlocks(blocks) }
diff --git a/common/app/routes/challenges/index.js b/common/app/routes/challenges/index.js index 9c56e425c9..5dfee03056 100644 --- a/common/app/routes/challenges/index.js +++ b/common/app/routes/challenges/index.js @@ -12,6 +12,11 @@ export const challenges = { } }; +export const modernChallenges = { + path: 'challenges/:block/:dashedName', + component: Show +}; + export const map = { path: 'map', component: ShowMap diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js index 0e62fc535c..43b6158c9e 100644 --- a/common/app/routes/challenges/redux/actions.js +++ b/common/app/routes/challenges/redux/actions.js @@ -9,7 +9,10 @@ export const goToStep = createAction(types.goToStep); export const completeAction = createAction(types.completeAction); // challenges -export const fetchChallenge = createAction(types.fetchChallenge); +export const fetchChallenge = createAction( + types.fetchChallenge, + (dashedName, block) => ({ dashedName, block }) +); export const fetchChallengeCompleted = createAction( types.fetchChallengeCompleted, (_, challenge) => challenge, diff --git a/common/app/routes/challenges/redux/completion-saga.js b/common/app/routes/challenges/redux/completion-saga.js index 52d8a69753..6a0d0aba02 100644 --- a/common/app/routes/challenges/redux/completion-saga.js +++ b/common/app/routes/challenges/redux/completion-saga.js @@ -1,20 +1,13 @@ import { Observable } from 'rx'; -import { push } from 'react-router-redux'; import types from './types'; -import { - showChallengeComplete, - moveToNextChallenge, - updateCurrentChallenge -} from './actions'; +import { showChallengeComplete, moveToNextChallenge } from './actions'; import { createErrorObservable, makeToast, updatePoints } from '../../../redux/actions'; -import { getNextChallenge } from '../utils'; import { challengeSelector } from './selectors'; - import { backEndProject } from '../../../utils/challengeTypes'; import { randomCompliment } from '../../../utils/get-words'; import { postJSON$ } from '../../../../utils/ajax-stream'; @@ -179,25 +172,13 @@ export default function completionSaga(actions$, getState) { return actions$ .filter(({ type }) => ( type === types.checkChallenge || - type === types.submitChallenge || - type === types.moveToNextChallenge + type === types.submitChallenge )) .flatMap(({ type, payload }) => { const state = getState(); const { submitType } = challengeSelector(state); const submitter = submitTypes[submitType] || (() => Observable.just(null)); - if (type === types.moveToNextChallenge) { - const nextChallenge = getNextChallenge( - state.challengesApp.challenge, - state.entities, - state.challengesApp.superBlocks - ); - return Observable.of( - updateCurrentChallenge(nextChallenge), - push(`/challenges/${nextChallenge.dashedName}`) - ); - } return submitter(type, state, payload); }); } diff --git a/common/app/routes/challenges/redux/fetch-challenges-saga.js b/common/app/routes/challenges/redux/fetch-challenges-saga.js index 5e6906dc2b..d568ab154d 100644 --- a/common/app/routes/challenges/redux/fetch-challenges-saga.js +++ b/common/app/routes/challenges/redux/fetch-challenges-saga.js @@ -1,7 +1,10 @@ import { Observable } from 'rx'; import { fetchChallenge, fetchChallenges } from './types'; import { - createErrorObserable, + delayedRedirect, + createErrorObserable +} from '../../../redux/actions'; +import { fetchChallengeCompleted, fetchChallengesCompleted, updateCurrentChallenge @@ -13,17 +16,18 @@ export default function fetchChallengesSaga(action$, getState, { services }) { type === fetchChallenges || type === fetchChallenge )) - .flatMap(({ type, payload })=> { + .flatMap(({ type, payload: { dashedName, block } = {} }) => { const options = { service: 'map' }; if (type === fetchChallenge) { - options.params = { dashedName: payload }; + options.params = { dashedName, block }; } return services.readService$(options) - .flatMap(({ entities, result } = {}) => { + .flatMap(({ entities, result, redirect } = {}) => { if (type === fetchChallenge) { return Observable.of( fetchChallengeCompleted(entities, result), - updateCurrentChallenge(entities.challenge[result]) + updateCurrentChallenge(entities.challenge[result.challenge]), + redirect ? delayedRedirect(redirect) : null ); } return Observable.just(fetchChallengesCompleted(entities, result)); diff --git a/common/app/routes/challenges/redux/next-challenge-saga.js b/common/app/routes/challenges/redux/next-challenge-saga.js new file mode 100644 index 0000000000..acea4a75f2 --- /dev/null +++ b/common/app/routes/challenges/redux/next-challenge-saga.js @@ -0,0 +1,23 @@ +import { Observable } from 'rx'; +import { push } from 'react-router-redux'; +import { moveToNextChallenge } from './types'; +import { getNextChallenge } from '../utils'; +import { updateCurrentChallenge } from './actions'; +// import { createErrorObservable, makeToast } from '../../../redux/actions'; + +export default function nextChallengeSaga(actions$, getState) { + return actions$ + .filter(({ type }) => type === moveToNextChallenge) + .flatMap(() => { + const state = getState(); + const nextChallenge = getNextChallenge( + state.challengesApp.challenge, + state.entities, + state.challengesApp.superBlocks + ); + return Observable.of( + updateCurrentChallenge(nextChallenge), + push(`/challenges/${nextChallenge.dashedName}`) + ); + }); +} diff --git a/common/app/routes/challenges/utils.js b/common/app/routes/challenges/utils.js index 8cf63ed7e1..ae4cb9fb52 100644 --- a/common/app/routes/challenges/utils.js +++ b/common/app/routes/challenges/utils.js @@ -61,7 +61,7 @@ export function getFileKey({ challengeType }) { export function createTests({ tests = [] }) { return tests .map(test => ({ - text: test.split('message: ').pop().replace(/\'\);/g, ''), + text: ('' + test).split('message: ').pop().replace(/\'\);/g, ''), testString: test })); } diff --git a/common/app/routes/index.js b/common/app/routes/index.js index 0f678defe6..bbb70e7438 100644 --- a/common/app/routes/index.js +++ b/common/app/routes/index.js @@ -1,6 +1,6 @@ import Jobs from './Jobs'; import Hikes from './Hikes'; -import { map, challenges } from './challenges'; +import { modernChallenges, map, challenges } from './challenges'; import NotFound from '../components/NotFound/index.jsx'; export default { @@ -9,6 +9,7 @@ export default { Jobs, Hikes, challenges, + modernChallenges, map, { path: '*', diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 8a33522d0e..a627c10630 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -64,10 +64,19 @@ export default function reactSubRouter(app) { ) .map(({ markup }) => ({ markup, store, epic })); }) + .filter(({ store, epic }) => { + const { delayedRedirect } = store.getState().app; + if (delayedRedirect) { + res.redirect(delayedRedirect); + epic.dispose(); + return false; + } + return true; + }) .flatMap(function({ markup, store, epic }) { log('react markup rendered, data fetched'); const state = store.getState(); - const { title } = state.app.title; + const { title } = state.app; epic.dispose(); res.expose(state, 'data'); return res.render$( diff --git a/server/services/map.js b/server/services/map.js index 95a5549c0c..cd5ef0e2a2 100644 --- a/server/services/map.js +++ b/server/services/map.js @@ -1,6 +1,7 @@ import { Observable } from 'rx'; import { Schema, valuesOf, arrayOf, normalize } from 'normalizr'; import { nameify, dasherize, unDasherize } from '../utils'; +import { dashify } from '../../common/utils'; import debug from 'debug'; const isDev = process.env.NODE_ENV !== 'production'; @@ -118,6 +119,41 @@ function getFirstChallenge(challengeMap$) { }); } +// this is a hard search +// falls back to soft search +function getChallengeAndBlock( + challengeDashedName, + blockDashedName, + challengeMap$ +) { + return challengeMap$ + .flatMap(({ entities }) => { + const block = entities.block[blockDashedName]; + const challenge = entities.challenge[challengeDashedName]; + if ( + !block || + !challenge || + !shouldNotFilterComingSoon(challenge) + ) { + return getChallengeByDashedName(challengeDashedName, challengeMap$); + } + return Observable.just({ + redirect: block.dashedName !== blockDashedName ? + `/challenges/${block.dashedName}/${challenge.dashedName}` : + false, + entities: { + challenge: { + [challenge.dashedName]: challenge + } + }, + result: { + block: block.dashedName, + challenge: challenge.dashedName + } + }); + }); +} + function getChallengeByDashedName(dashedName, challengeMap$) { const challengeName = unDasherize(dashedName) .replace(challengesRegex, ''); @@ -142,8 +178,13 @@ function getChallengeByDashedName(dashedName, challengeMap$) { return getFirstChallenge(challengeMap$); }) .map(challenge => ({ + redirect: + `/challenges/${dashify(challenge.block)}/${challenge.dashedName}`, entities: { challenge: { [challenge.dashedName]: challenge } }, - result: challenge.dashedName + result: { + challenge: challenge.dashedName, + block: dashify(challenge.block) + } })); } @@ -152,7 +193,11 @@ export default function mapService(app) { const challengeMap$ = cachedMap(Block); return { name: 'map', - read: (req, resource, { dashedName } = {}, config, cb) => { + read: (req, resource, { block, dashedName } = {}, config, cb) => { + if (block && dashedName) { + return getChallengeAndBlock(dashedName, block, challengeMap$) + .subscribe(challenge => cb(null, challenge), cb); + } if (dashedName) { return getChallengeByDashedName(dashedName, challengeMap$) .subscribe(challenge => cb(null, challenge), cb);