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
|
// updatePoints(points: Number) => Action
|
||||||
export const updatePoints = createAction(types.updatePoints);
|
export const updatePoints = createAction(types.updatePoints);
|
||||||
|
// used when server needs client to redirect
|
||||||
|
export const delayedRedirect = createAction(types.delayedRedirect);
|
||||||
|
|
||||||
// hardGoTo(path: String) => Action
|
// hardGoTo(path: String) => Action
|
||||||
export const hardGoTo = createAction(types.hardGoTo);
|
export const hardGoTo = createAction(types.hardGoTo);
|
||||||
|
@ -55,6 +55,10 @@ export default handleActions(
|
|||||||
[types.toggleMainChat]: state => ({
|
[types.toggleMainChat]: state => ({
|
||||||
...state,
|
...state,
|
||||||
isMainChatOpen: !state.isMainChatOpen
|
isMainChatOpen: !state.isMainChatOpen
|
||||||
|
}),
|
||||||
|
[types.delayedRedirect]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
delayedRedirect: payload
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
|
@ -11,6 +11,7 @@ export default createTypes([
|
|||||||
'handleError',
|
'handleError',
|
||||||
// used to hit the server
|
// used to hit the server
|
||||||
'hardGoTo',
|
'hardGoTo',
|
||||||
|
'delayedRedirect',
|
||||||
|
|
||||||
'initWindowHeight',
|
'initWindowHeight',
|
||||||
'updateWindowHeight',
|
'updateWindowHeight',
|
||||||
|
@ -35,8 +35,8 @@ const mapStateToProps = createSelector(
|
|||||||
|
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
fetchAction: 'fetchChallenge',
|
fetchAction: 'fetchChallenge',
|
||||||
getActionArgs({ params: { dashedName } }) {
|
getActionArgs({ params: { block, dashedName } }) {
|
||||||
return [ dashedName ];
|
return [ dashedName, block ];
|
||||||
},
|
},
|
||||||
isPrimed({ challenge }) {
|
isPrimed({ challenge }) {
|
||||||
return !!challenge;
|
return !!challenge;
|
||||||
|
@ -13,12 +13,13 @@ export class Block extends PureComponent {
|
|||||||
static displayName = 'Block';
|
static displayName = 'Block';
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
dashedName: PropTypes.string,
|
||||||
time: PropTypes.string,
|
time: PropTypes.string,
|
||||||
challenges: PropTypes.array,
|
challenges: PropTypes.array,
|
||||||
updateCurrentChallenge: PropTypes.func
|
updateCurrentChallenge: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderChallenges(challenges, updateCurrentChallenge) {
|
renderChallenges(blockName, challenges, updateCurrentChallenge) {
|
||||||
if (!Array.isArray(challenges) || !challenges.length) {
|
if (!Array.isArray(challenges) || !challenges.length) {
|
||||||
return <div>No Challenges Found</div>;
|
return <div>No Challenges Found</div>;
|
||||||
}
|
}
|
||||||
@ -37,7 +38,8 @@ export class Block extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className={ challengeClassName }
|
className={ challengeClassName }
|
||||||
key={ title }>
|
key={ title }
|
||||||
|
>
|
||||||
{ title }
|
{ title }
|
||||||
{
|
{
|
||||||
isRequired ?
|
isRequired ?
|
||||||
@ -50,10 +52,12 @@ export class Block extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className={ challengeClassName }
|
className={ challengeClassName }
|
||||||
key={ title }>
|
key={ title }
|
||||||
<Link to={ `/challenges/${dashedName}` }>
|
>
|
||||||
|
<Link to={ `/challenges/${blockName}/${dashedName}` }>
|
||||||
<span
|
<span
|
||||||
onClick={ () => updateCurrentChallenge(challenge) }>
|
onClick={ () => updateCurrentChallenge(challenge) }
|
||||||
|
>
|
||||||
{ title }
|
{ title }
|
||||||
<span className='sr-only'>complete</span>
|
<span className='sr-only'>complete</span>
|
||||||
{
|
{
|
||||||
@ -69,7 +73,13 @@ export class Block extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { title, time, challenges, updateCurrentChallenge } = this.props;
|
const {
|
||||||
|
title,
|
||||||
|
time,
|
||||||
|
challenges,
|
||||||
|
updateCurrentChallenge,
|
||||||
|
dashedName
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
bsClass='map-accordion-panel-nested'
|
bsClass='map-accordion-panel-nested'
|
||||||
@ -82,8 +92,11 @@ export class Block extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
id={ title }
|
id={ title }
|
||||||
key={ title }>
|
key={ title }
|
||||||
{ this.renderChallenges(challenges, updateCurrentChallenge) }
|
>
|
||||||
|
{
|
||||||
|
this.renderChallenges(dashedName, challenges, updateCurrentChallenge)
|
||||||
|
}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,8 @@ export default class SuperBlock extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<Block
|
<Block
|
||||||
key={ block.title }
|
key={ block.title }
|
||||||
{ ...block } />
|
{ ...block }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -35,7 +36,8 @@ export default class SuperBlock extends PureComponent {
|
|||||||
expanded={ true }
|
expanded={ true }
|
||||||
header={ <h2><FA name='caret-right' />{ title }</h2> }
|
header={ <h2><FA name='caret-right' />{ title }</h2> }
|
||||||
id={ title }
|
id={ title }
|
||||||
key={ title }>
|
key={ title }
|
||||||
|
>
|
||||||
{
|
{
|
||||||
message ?
|
message ?
|
||||||
<div className='challenge-block-description'>
|
<div className='challenge-block-description'>
|
||||||
@ -44,7 +46,8 @@ export default class SuperBlock extends PureComponent {
|
|||||||
''
|
''
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
className='map-accordion-block'>
|
className='map-accordion-block'
|
||||||
|
>
|
||||||
{ this.renderBlocks(blocks) }
|
{ this.renderBlocks(blocks) }
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
@ -12,6 +12,11 @@ export const challenges = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const modernChallenges = {
|
||||||
|
path: 'challenges/:block/:dashedName',
|
||||||
|
component: Show
|
||||||
|
};
|
||||||
|
|
||||||
export const map = {
|
export const map = {
|
||||||
path: 'map',
|
path: 'map',
|
||||||
component: ShowMap
|
component: ShowMap
|
||||||
|
@ -9,7 +9,10 @@ export const goToStep = createAction(types.goToStep);
|
|||||||
export const completeAction = createAction(types.completeAction);
|
export const completeAction = createAction(types.completeAction);
|
||||||
|
|
||||||
// challenges
|
// challenges
|
||||||
export const fetchChallenge = createAction(types.fetchChallenge);
|
export const fetchChallenge = createAction(
|
||||||
|
types.fetchChallenge,
|
||||||
|
(dashedName, block) => ({ dashedName, block })
|
||||||
|
);
|
||||||
export const fetchChallengeCompleted = createAction(
|
export const fetchChallengeCompleted = createAction(
|
||||||
types.fetchChallengeCompleted,
|
types.fetchChallengeCompleted,
|
||||||
(_, challenge) => challenge,
|
(_, challenge) => challenge,
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { push } from 'react-router-redux';
|
|
||||||
import types from './types';
|
import types from './types';
|
||||||
import {
|
import { showChallengeComplete, moveToNextChallenge } from './actions';
|
||||||
showChallengeComplete,
|
|
||||||
moveToNextChallenge,
|
|
||||||
updateCurrentChallenge
|
|
||||||
} from './actions';
|
|
||||||
import {
|
import {
|
||||||
createErrorObservable,
|
createErrorObservable,
|
||||||
makeToast,
|
makeToast,
|
||||||
updatePoints
|
updatePoints
|
||||||
} from '../../../redux/actions';
|
} from '../../../redux/actions';
|
||||||
|
|
||||||
import { getNextChallenge } from '../utils';
|
|
||||||
import { challengeSelector } from './selectors';
|
import { challengeSelector } from './selectors';
|
||||||
|
|
||||||
import { backEndProject } from '../../../utils/challengeTypes';
|
import { backEndProject } from '../../../utils/challengeTypes';
|
||||||
import { randomCompliment } from '../../../utils/get-words';
|
import { randomCompliment } from '../../../utils/get-words';
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
@ -179,25 +172,13 @@ export default function completionSaga(actions$, getState) {
|
|||||||
return actions$
|
return actions$
|
||||||
.filter(({ type }) => (
|
.filter(({ type }) => (
|
||||||
type === types.checkChallenge ||
|
type === types.checkChallenge ||
|
||||||
type === types.submitChallenge ||
|
type === types.submitChallenge
|
||||||
type === types.moveToNextChallenge
|
|
||||||
))
|
))
|
||||||
.flatMap(({ type, payload }) => {
|
.flatMap(({ type, payload }) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { submitType } = challengeSelector(state);
|
const { submitType } = challengeSelector(state);
|
||||||
const submitter = submitTypes[submitType] ||
|
const submitter = submitTypes[submitType] ||
|
||||||
(() => Observable.just(null));
|
(() => 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);
|
return submitter(type, state, payload);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { fetchChallenge, fetchChallenges } from './types';
|
import { fetchChallenge, fetchChallenges } from './types';
|
||||||
import {
|
import {
|
||||||
createErrorObserable,
|
delayedRedirect,
|
||||||
|
createErrorObserable
|
||||||
|
} from '../../../redux/actions';
|
||||||
|
import {
|
||||||
fetchChallengeCompleted,
|
fetchChallengeCompleted,
|
||||||
fetchChallengesCompleted,
|
fetchChallengesCompleted,
|
||||||
updateCurrentChallenge
|
updateCurrentChallenge
|
||||||
@ -13,17 +16,18 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
|
|||||||
type === fetchChallenges ||
|
type === fetchChallenges ||
|
||||||
type === fetchChallenge
|
type === fetchChallenge
|
||||||
))
|
))
|
||||||
.flatMap(({ type, payload })=> {
|
.flatMap(({ type, payload: { dashedName, block } = {} }) => {
|
||||||
const options = { service: 'map' };
|
const options = { service: 'map' };
|
||||||
if (type === fetchChallenge) {
|
if (type === fetchChallenge) {
|
||||||
options.params = { dashedName: payload };
|
options.params = { dashedName, block };
|
||||||
}
|
}
|
||||||
return services.readService$(options)
|
return services.readService$(options)
|
||||||
.flatMap(({ entities, result } = {}) => {
|
.flatMap(({ entities, result, redirect } = {}) => {
|
||||||
if (type === fetchChallenge) {
|
if (type === fetchChallenge) {
|
||||||
return Observable.of(
|
return Observable.of(
|
||||||
fetchChallengeCompleted(entities, result),
|
fetchChallengeCompleted(entities, result),
|
||||||
updateCurrentChallenge(entities.challenge[result])
|
updateCurrentChallenge(entities.challenge[result.challenge]),
|
||||||
|
redirect ? delayedRedirect(redirect) : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Observable.just(fetchChallengesCompleted(entities, result));
|
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 = [] }) {
|
export function createTests({ tests = [] }) {
|
||||||
return tests
|
return tests
|
||||||
.map(test => ({
|
.map(test => ({
|
||||||
text: test.split('message: ').pop().replace(/\'\);/g, ''),
|
text: ('' + test).split('message: ').pop().replace(/\'\);/g, ''),
|
||||||
testString: test
|
testString: test
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Jobs from './Jobs';
|
import Jobs from './Jobs';
|
||||||
import Hikes from './Hikes';
|
import Hikes from './Hikes';
|
||||||
import { map, challenges } from './challenges';
|
import { modernChallenges, map, challenges } from './challenges';
|
||||||
import NotFound from '../components/NotFound/index.jsx';
|
import NotFound from '../components/NotFound/index.jsx';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -9,6 +9,7 @@ export default {
|
|||||||
Jobs,
|
Jobs,
|
||||||
Hikes,
|
Hikes,
|
||||||
challenges,
|
challenges,
|
||||||
|
modernChallenges,
|
||||||
map,
|
map,
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
|
@ -64,10 +64,19 @@ export default function reactSubRouter(app) {
|
|||||||
)
|
)
|
||||||
.map(({ markup }) => ({ markup, store, epic }));
|
.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 }) {
|
.flatMap(function({ markup, store, epic }) {
|
||||||
log('react markup rendered, data fetched');
|
log('react markup rendered, data fetched');
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const { title } = state.app.title;
|
const { title } = state.app;
|
||||||
epic.dispose();
|
epic.dispose();
|
||||||
res.expose(state, 'data');
|
res.expose(state, 'data');
|
||||||
return res.render$(
|
return res.render$(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
|
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
|
||||||
import { nameify, dasherize, unDasherize } from '../utils';
|
import { nameify, dasherize, unDasherize } from '../utils';
|
||||||
|
import { dashify } from '../../common/utils';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
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$) {
|
function getChallengeByDashedName(dashedName, challengeMap$) {
|
||||||
const challengeName = unDasherize(dashedName)
|
const challengeName = unDasherize(dashedName)
|
||||||
.replace(challengesRegex, '');
|
.replace(challengesRegex, '');
|
||||||
@ -142,8 +178,13 @@ function getChallengeByDashedName(dashedName, challengeMap$) {
|
|||||||
return getFirstChallenge(challengeMap$);
|
return getFirstChallenge(challengeMap$);
|
||||||
})
|
})
|
||||||
.map(challenge => ({
|
.map(challenge => ({
|
||||||
|
redirect:
|
||||||
|
`/challenges/${dashify(challenge.block)}/${challenge.dashedName}`,
|
||||||
entities: { challenge: { [challenge.dashedName]: challenge } },
|
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);
|
const challengeMap$ = cachedMap(Block);
|
||||||
return {
|
return {
|
||||||
name: 'map',
|
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) {
|
if (dashedName) {
|
||||||
return getChallengeByDashedName(dashedName, challengeMap$)
|
return getChallengeByDashedName(dashedName, challengeMap$)
|
||||||
.subscribe(challenge => cb(null, challenge), cb);
|
.subscribe(challenge => cb(null, challenge), cb);
|
||||||
|
Reference in New Issue
Block a user