Add block scoping to challenges url
This commit is contained in:
@ -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);
|
||||
|
@ -55,6 +55,10 @@ export default handleActions(
|
||||
[types.toggleMainChat]: state => ({
|
||||
...state,
|
||||
isMainChatOpen: !state.isMainChatOpen
|
||||
}),
|
||||
[types.delayedRedirect]: (state, { payload }) => ({
|
||||
...state,
|
||||
delayedRedirect: payload
|
||||
})
|
||||
},
|
||||
initialState
|
||||
|
@ -11,6 +11,7 @@ export default createTypes([
|
||||
'handleError',
|
||||
// used to hit the server
|
||||
'hardGoTo',
|
||||
'delayedRedirect',
|
||||
|
||||
'initWindowHeight',
|
||||
'updateWindowHeight',
|
||||
|
@ -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;
|
||||
|
@ -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 <div>No Challenges Found</div>;
|
||||
}
|
||||
@ -37,7 +38,8 @@ export class Block extends PureComponent {
|
||||
return (
|
||||
<p
|
||||
className={ challengeClassName }
|
||||
key={ title }>
|
||||
key={ title }
|
||||
>
|
||||
{ title }
|
||||
{
|
||||
isRequired ?
|
||||
@ -50,10 +52,12 @@ export class Block extends PureComponent {
|
||||
return (
|
||||
<p
|
||||
className={ challengeClassName }
|
||||
key={ title }>
|
||||
<Link to={ `/challenges/${dashedName}` }>
|
||||
key={ title }
|
||||
>
|
||||
<Link to={ `/challenges/${blockName}/${dashedName}` }>
|
||||
<span
|
||||
onClick={ () => updateCurrentChallenge(challenge) }>
|
||||
onClick={ () => updateCurrentChallenge(challenge) }
|
||||
>
|
||||
{ title }
|
||||
<span className='sr-only'>complete</span>
|
||||
{
|
||||
@ -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 (
|
||||
<Panel
|
||||
bsClass='map-accordion-panel-nested'
|
||||
@ -82,8 +92,11 @@ export class Block extends PureComponent {
|
||||
</div>
|
||||
}
|
||||
id={ title }
|
||||
key={ title }>
|
||||
{ this.renderChallenges(challenges, updateCurrentChallenge) }
|
||||
key={ title }
|
||||
>
|
||||
{
|
||||
this.renderChallenges(dashedName, challenges, updateCurrentChallenge)
|
||||
}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ export default class SuperBlock extends PureComponent {
|
||||
return (
|
||||
<Block
|
||||
key={ block.title }
|
||||
{ ...block } />
|
||||
{ ...block }
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -35,7 +36,8 @@ export default class SuperBlock extends PureComponent {
|
||||
expanded={ true }
|
||||
header={ <h2><FA name='caret-right' />{ title }</h2> }
|
||||
id={ title }
|
||||
key={ title }>
|
||||
key={ title }
|
||||
>
|
||||
{
|
||||
message ?
|
||||
<div className='challenge-block-description'>
|
||||
@ -44,7 +46,8 @@ export default class SuperBlock extends PureComponent {
|
||||
''
|
||||
}
|
||||
<div
|
||||
className='map-accordion-block'>
|
||||
className='map-accordion-block'
|
||||
>
|
||||
{ this.renderBlocks(blocks) }
|
||||
</div>
|
||||
</Panel>
|
||||
|
@ -12,6 +12,11 @@ export const challenges = {
|
||||
}
|
||||
};
|
||||
|
||||
export const modernChallenges = {
|
||||
path: 'challenges/:block/:dashedName',
|
||||
component: Show
|
||||
};
|
||||
|
||||
export const map = {
|
||||
path: 'map',
|
||||
component: ShowMap
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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));
|
||||
|
23
common/app/routes/challenges/redux/next-challenge-saga.js
Normal file
23
common/app/routes/challenges/redux/next-challenge-saga.js
Normal file
@ -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}`)
|
||||
);
|
||||
});
|
||||
}
|
@ -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
|
||||
}));
|
||||
}
|
||||
|
@ -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: '*',
|
||||
|
@ -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$(
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user