Add block scoping to challenges url

This commit is contained in:
Berkeley Martinez
2016-06-09 16:02:51 -07:00
parent 91dc3625d9
commit acf4d99f67
15 changed files with 139 additions and 45 deletions

View File

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

View File

@ -55,6 +55,10 @@ export default handleActions(
[types.toggleMainChat]: state => ({
...state,
isMainChatOpen: !state.isMainChatOpen
}),
[types.delayedRedirect]: (state, { payload }) => ({
...state,
delayedRedirect: payload
})
},
initialState

View File

@ -11,6 +11,7 @@ export default createTypes([
'handleError',
// used to hit the server
'hardGoTo',
'delayedRedirect',
'initWindowHeight',
'updateWindowHeight',

View File

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

View File

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

View File

@ -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>

View File

@ -12,6 +12,11 @@ export const challenges = {
}
};
export const modernChallenges = {
path: 'challenges/:block/:dashedName',
component: Show
};
export const map = {
path: 'map',
component: ShowMap

View File

@ -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,

View File

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

View File

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

View 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}`)
);
});
}

View File

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

View File

@ -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: '*',

View File

@ -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$(

View File

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