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);