Merge pull request #16771 from Bouncey/feat/splitMapService

Feat(challenge): Only send the challenges for the requested block
This commit is contained in:
Berkeley Martinez
2018-02-26 11:31:01 -08:00
committed by GitHub
18 changed files with 421 additions and 194 deletions

View File

@ -9,13 +9,16 @@ import ns from './ns.json';
import Challenges from './Challenges.jsx';
import {
toggleThisPanel,
makePanelOpenSelector
} from './redux';
import { fetchNewBlock } from '../redux';
import { makeBlockSelector } from '../entities';
const dispatchActions = { toggleThisPanel };
const mapDispatchToProps = {
fetchNewBlock,
toggleThisPanel
};
function makeMapStateToProps(_, { dashedName }) {
return createSelector(
makeBlockSelector(dashedName),
@ -34,6 +37,7 @@ function makeMapStateToProps(_, { dashedName }) {
const propTypes = {
challenges: PropTypes.array,
dashedName: PropTypes.string,
fetchNewBlock: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
time: PropTypes.string,
title: PropTypes.string,
@ -74,7 +78,8 @@ export class Block extends PureComponent {
time,
dashedName,
isOpen,
challenges
challenges,
fetchNewBlock
} = this.props;
return (
<Panel
@ -85,6 +90,7 @@ export class Block extends PureComponent {
header={ this.renderHeader(isOpen, title, time) }
id={ title }
key={ title }
onClick={ () => fetchNewBlock(dashedName) }
onSelect={ this.handleSelect }
>
{ isOpen && <Challenges challenges={ challenges } /> }
@ -96,4 +102,4 @@ export class Block extends PureComponent {
Block.displayName = 'Block';
Block.propTypes = propTypes;
export default connect(makeMapStateToProps, dispatchActions)(Block);
export default connect(makeMapStateToProps, mapDispatchToProps)(Block);

View File

@ -4,23 +4,32 @@ import { connect } from 'react-redux';
import { Col, Row } from 'react-bootstrap';
import ns from './ns.json';
import { Loader } from '../helperComponents';
import SuperBlock from './Super-Block.jsx';
import { superBlocksSelector } from '../redux';
import { fetchMapUi } from './redux';
const mapStateToProps = state => ({
superBlocks: superBlocksSelector(state)
});
const mapDispatchToProps = {};
const mapDispatchToProps = { fetchMapUi };
const propTypes = {
fetchMapUi: PropTypes.func.isRequired,
params: PropTypes.object,
superBlocks: PropTypes.array
};
export class ShowMap extends PureComponent {
renderSuperBlocks(superBlocks) {
renderSuperBlocks() {
const { superBlocks } = this.props;
if (!Array.isArray(superBlocks) || !superBlocks.length) {
return <div>No Super Blocks</div>;
return (
<div style={{ height: '300px' }}>
<Loader />
</div>
);
}
return superBlocks.map(dashedName => (
<SuperBlock
@ -31,12 +40,11 @@ export class ShowMap extends PureComponent {
}
render() {
const { superBlocks } = this.props;
return (
<Row>
<Col xs={ 12 }>
<div className={ `${ns}-accordion center-block` }>
{ this.renderSuperBlocks(superBlocks) }
{ this.renderSuperBlocks() }
<div className='spacer' />
</div>
</Col>

View File

@ -27,8 +27,7 @@ function mapStateToProps(_, { dashedName }) {
isOpen,
dashedName,
title: superBlock.title || dashedName,
blocks: superBlock.blocks || [],
message: superBlock.message
blocks: superBlock.blocks || []
})
);
}
@ -37,7 +36,6 @@ const propTypes = {
blocks: PropTypes.array,
dashedName: PropTypes.string,
isOpen: PropTypes.bool,
message: PropTypes.string,
title: PropTypes.string,
toggleThisPanel: PropTypes.func
};
@ -52,17 +50,6 @@ export class SuperBlock extends PureComponent {
this.props.toggleThisPanel(eventKey);
}
renderMessage(message) {
if (!message) {
return null;
}
return (
<div className={ `${ns}-block-description` }>
{ message }
</div>
);
}
renderHeader(isOpen, title, isCompleted) {
return (
<div className={ isCompleted ? 'faded' : '' }>
@ -81,7 +68,6 @@ export class SuperBlock extends PureComponent {
title,
dashedName,
blocks,
message,
isOpen
} = this.props;
return (
@ -95,7 +81,6 @@ export class SuperBlock extends PureComponent {
key={ dashedName || title }
onSelect={ this.handleSelect }
>
{ this.renderMessage(message) }
<Blocks blocks={ blocks } />
</Panel>
);

View File

@ -0,0 +1,41 @@
import { ofType } from 'redux-epic';
import debug from 'debug';
import {
types as appTypes,
createErrorObservable
} from '../../redux';
import { types, fetchMapUiComplete } from './';
import { langSelector } from '../../Router/redux';
import { shapeChallenges } from '../../redux/utils';
const isDev = debug.enabled('fcc:*');
export default function fetchMapUiEpic(
actions,
{ getState },
{ services }
) {
return actions::ofType(
appTypes.appMounted,
types.fetchMapUi.start
)
.flatMapLatest(() => {
const lang = langSelector(getState());
const options = {
params: { lang },
service: 'map-ui'
};
return services.readService$(options)
.retry(3)
.map(({ entities, ...res }) => ({
entities: shapeChallenges(
entities,
isDev
),
...res
}))
.map(fetchMapUiComplete)
.catch(createErrorObservable);
});
}

View File

@ -1,25 +1,26 @@
import {
createAction,
createAsyncTypes,
createTypes,
handleActions
} from 'berkeleys-redux-utils';
import { createSelector } from 'reselect';
import noop from 'lodash/noop';
import capitalize from 'lodash/capitalize';
import { capitalize, noop } from 'lodash';
import * as utils from './utils.js';
import ns from '../ns.json';
import {
types as app,
createEventMetaCreator
} from '../../redux';
export const epics = [];
import fetchMapUiEpic from './fetch-map-ui-epic';
export const epics = [ fetchMapUiEpic ];
export const types = createTypes([
'onRouteMap',
'initMap',
createAsyncTypes('fetchMapUi'),
'toggleThisPanel',
'isAllCollapsed',
@ -31,6 +32,9 @@ export const types = createTypes([
export const initMap = createAction(types.initMap);
export const fetchMapUi = createAction(types.fetchMapUi.start);
export const fetchMapUiComplete = createAction(types.fetchMapUi.complete);
export const toggleThisPanel = createAction(types.toggleThisPanel);
export const collapseAll = createAction(types.collapseAll);
@ -100,10 +104,11 @@ export default handleActions(
mapUi
};
},
[app.fetchChallenges.complete]: (state, { payload }) => {
[types.fetchMapUi.complete]: (state, { payload }) => {
const { entities, result } = payload;
return {
...state,
...result,
mapUi: utils.createMapUi(entities, result)
};
}

View File

@ -1,6 +1,7 @@
import { findIndex, invert, pick, property } from 'lodash';
import { findIndex, invert, pick, property, merge, union } from 'lodash';
import uuid from 'uuid/v4';
import {
combineActions,
composeReducers,
createAction,
createTypes,
@ -8,8 +9,9 @@ import {
} from 'berkeleys-redux-utils';
import { themes } from '../../utils/themes';
import { usernameSelector, types as app } from '../redux';
import { types as challenges } from '../routes/Challenges/redux';
import { usernameSelector } from '../redux';
import { types as map } from '../Map/redux';
export const ns = 'entities';
export const getNS = state => state[ns];
@ -85,7 +87,8 @@ const defaultState = {
superBlock: {},
block: {},
challenge: {},
user: {}
user: {},
fullBlocks: []
};
export function selectiveChallengeTitleSelector(state, dashedName) {
@ -148,6 +151,8 @@ export function makeSuperBlockSelector(name) {
export const isChallengeLoaded = (state, { dashedName }) =>
!!challengeMapSelector(state)[dashedName];
export const fullBlocksSelector = state => getNS(state).fullBlocks;
export default composeReducers(
ns,
function metaReducer(state = defaultState, action) {
@ -163,8 +168,7 @@ export default composeReducers(
};
}
return {
...state,
...action.meta.entities
...merge(state, action.meta.entities)
};
}
return state;
@ -187,6 +191,18 @@ export default composeReducers(
},
handleActions(
() => ({
[
combineActions(
app.fetchChallenges.complete,
map.fetchMapUi.complete
)
]: (state, { payload }) => {
const {entities: { block } } = payload;
return {
...merge(state, payload.entities),
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
};
},
[
challenges.submitChallenge.complete
]: (state, { payload: { username, points, challengeInfo } }) => ({

View File

@ -9,9 +9,15 @@ import {
delayedRedirect,
fetchChallengeCompleted,
fetchChallengesCompleted
fetchChallengesCompleted,
fetchNewBlock,
challengeSelector,
nextChallengeSelector
} from './';
import { isChallengeLoaded } from '../entities/index.js';
import {
isChallengeLoaded,
fullBlocksSelector
} from '../entities';
import { shapeChallenges } from './utils';
import { types as challenge } from '../routes/Challenges/redux';
@ -19,12 +25,12 @@ import { langSelector } from '../Router/redux';
const isDev = debug.enabled('fcc:*');
export function fetchChallengeEpic(actions, { getState }, { services }) {
function fetchChallengeEpic(actions, { getState }, { services }) {
return actions::ofType(challenge.onRouteChallenges)
.filter(({ payload }) => !isChallengeLoaded(getState(), payload))
.flatMapLatest(({ payload: params }) => {
const options = {
service: 'map',
service: 'challenge',
params
};
return services.readService$(options)
@ -49,39 +55,65 @@ export function fetchChallengeEpic(actions, { getState }, { services }) {
});
}
export function fetchChallengesEpic(
export function fetchChallengesForBlockEpic(
actions,
{ getState },
{ services }
) {
return actions::ofType(
types.appMounted,
types.updateChallenges
types.updateChallenges,
types.fetchNewBlock.start
)
.flatMapLatest(() => {
const lang = langSelector(getState());
.flatMapLatest(({ type, payload }) => {
const fetchAnotherBlock = type === types.fetchNewBlock.start;
const state = getState();
let { block: blockName } = challengeSelector(state);
const lang = langSelector(state);
if (fetchAnotherBlock) {
const fullBlocks = fullBlocksSelector(state);
if (fullBlocks.includes(payload)) {
return Observable.of(null);
}
blockName = payload;
}
const options = {
params: { lang },
service: 'map'
params: { lang, blockName },
service: 'challenge'
};
return services.readService$(options)
.retry(3)
.map(({ entities, ...res }) => ({
entities: shapeChallenges(
entities,
isDev
),
...res
}))
.map(({ entities, result } = {}) => {
return fetchChallengesCompleted(
entities,
result
);
})
.map(fetchChallengesCompleted)
.startWith({ type: types.fetchChallenges.start })
.catch(createErrorObservable);
});
})
.filter(Boolean);
}
export default combineEpics(fetchChallengeEpic, fetchChallengesEpic);
function fetchChallengesForNextBlockEpic(action$, { getState }) {
return action$::ofType(challenge.checkForNextBlock)
.map(() => {
const {
nextChallenge,
isNewBlock,
isNewSuperBlock
} = nextChallengeSelector(getState());
const isNewBlockRequired = (
(isNewBlock || isNewSuperBlock) &&
nextChallenge &&
!nextChallenge.description
);
return isNewBlockRequired ?
fetchNewBlock(nextChallenge.block) :
null;
})
.filter(Boolean);
}
export default combineEpics(
fetchChallengeEpic,
fetchChallengesForBlockEpic,
fetchChallengesForNextBlockEpic
);

View File

@ -1,4 +1,4 @@
import _ from 'lodash';
import { flow, identity } from 'lodash';
import { Observable } from 'rx';
import {
combineActions,
@ -8,6 +8,7 @@ import {
handleActions
} from 'berkeleys-redux-utils';
import { createSelector } from 'reselect';
import debug from 'debug';
import fetchUserEpic from './fetch-user-epic.js';
import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
@ -19,12 +20,20 @@ import { updateThemeMetacreator, entitiesSelector } from '../entities';
import { utils } from '../Flash/redux';
import { paramsSelector } from '../Router/redux';
import { types as challenges } from '../routes/Challenges/redux';
import { challengeToFiles } from '../routes/Challenges/utils';
import { types as map } from '../Map/redux';
import {
challengeToFiles,
getFirstChallengeOfNextBlock,
getFirstChallengeOfNextSuperBlock,
getNextChallenge
} from '../routes/Challenges/utils';
import ns from '../ns.json';
import { themes, invertTheme } from '../../utils/themes.js';
const isDev = debug.enabled('fcc:*');
export const epics = [
fetchChallengesEpic,
fetchUserEpic,
@ -41,6 +50,7 @@ export const types = createTypes([
createAsyncTypes('fetchChallenge'),
createAsyncTypes('fetchChallenges'),
createAsyncTypes('fetchNewBlock'),
'updateChallenges',
createAsyncTypes('fetchOtherUser'),
createAsyncTypes('fetchUser'),
@ -66,7 +76,7 @@ const throwIfUndefined = () => {
// label?: String,
// value?: Number
// }) => () => Object
export const createEventMetaCreator = ({
export function createEventMetaCreator({
// categories are features or namespaces of the app (capitalized):
// Map, Nav, Challenges, and so on
category = throwIfUndefined,
@ -80,7 +90,8 @@ export const createEventMetaCreator = ({
label,
// used to tack some specific value for a GA event
value
} = throwIfUndefined) => () => ({
} = throwIfUndefined) {
return () => ({
analytics: {
type: 'event',
category,
@ -89,6 +100,7 @@ export const createEventMetaCreator = ({
value
}
});
}
export const onRouteHome = createAction(types.onRouteHome);
export const appMounted = createAction(types.appMounted);
@ -101,15 +113,20 @@ export const fetchChallengeCompleted = createAction(
null,
meta => ({
...meta,
..._.flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
...flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
})
);
export const fetchChallenges = createAction('' + types.fetchChallenges);
export const fetchChallengesCompleted = createAction(
types.fetchChallenges.complete,
(entities, result) => ({ entities, result }),
entities => ({ entities })
types.fetchChallenges.complete
);
export const fetchNewBlock = createAction(types.fetchNewBlock.start);
export const fetchNewBlockComplete = createAction(
types.fetchNewBlock.complete,
({ entities }) => entities
);
export const updateChallenges = createAction(types.updateChallenges);
// updateTitle(title: String) => Action
@ -122,7 +139,7 @@ export const fetchOtherUser = createAction(types.fetchOtherUser.start);
export const fetchOtherUserComplete = createAction(
types.fetchOtherUser.complete,
({ result }) => result,
_.identity
identity
);
// fetchUser() => Action
@ -131,7 +148,7 @@ export const fetchUser = createAction(types.fetchUser);
export const fetchUserComplete = createAction(
types.fetchUser.complete,
({ result }) => result,
_.identity
identity
);
export const showSignIn = createAction(types.showSignIn);
@ -209,7 +226,7 @@ export const userByNameSelector = state => {
return userMap[username] || {};
};
export const themeSelector = _.flow(
export const themeSelector = flow(
userSelector,
user => user.theme || themes.default
);
@ -262,6 +279,40 @@ export const firstChallengeSelector = createSelector(
}
);
export const nextChallengeSelector = state => {
let nextChallenge = {};
let isNewBlock = false;
let isNewSuperBlock = false;
const challenge = currentChallengeSelector(state);
const superBlocks = superBlocksSelector(state);
const entities = entitiesSelector(state);
nextChallenge = getNextChallenge(challenge, entities, { isDev });
// block completed.
if (!nextChallenge) {
isNewBlock = true;
nextChallenge = getFirstChallengeOfNextBlock(
challenge,
entities,
{ isDev }
);
}
// superBlock completed
if (!nextChallenge) {
isNewSuperBlock = true;
nextChallenge = getFirstChallengeOfNextSuperBlock(
challenge,
entities,
superBlocks,
{ isDev }
);
}
return {
nextChallenge,
isNewBlock,
isNewSuperBlock
};
};
export default handleActions(
() => ({
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
@ -275,7 +326,7 @@ export default handleActions(
}),
[combineActions(
types.fetchChallenge.complete,
types.fetchChallenges.complete
map.fetchMapUi.complete
)]: (state, { payload }) => ({
...state,
superBlocks: payload.result.superBlocks

View File

@ -1,5 +1,6 @@
import flowRight from 'lodash/flowRight';
import { createNameIdMap } from '../../utils/map.js';
import { partial } from 'lodash';
export function filterComingSoonBetaChallenge(
isDev = false,
@ -13,7 +14,7 @@ export function filterComingSoonBetaFromEntities(
{ challenge: challengeMap, block: blockMap = {}, ...rest },
isDev = false
) {
const filter = filterComingSoonBetaChallenge.bind(null, isDev);
const filter = partial(filterComingSoonBetaChallenge, isDev);
return {
...rest,
block: Object.keys(blockMap)

View File

@ -11,6 +11,8 @@ import {
closeChallengeModal,
submitChallenge,
checkForNextBlock,
challengeModalSelector,
successMessageSelector
} from './redux';
@ -34,12 +36,14 @@ const mapDispatchToProps = function(dispatch) {
},
submitChallenge: () => {
dispatch(submitChallenge());
}
},
checkForNextBlock: () => dispatch(checkForNextBlock())
};
return () => dispatchers;
};
const propTypes = {
checkForNextBlock: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
handleKeypress: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
@ -48,6 +52,11 @@ const propTypes = {
};
export class CompletionModal extends PureComponent {
componentDidUpdate() {
if (this.props.isOpen) {
this.props.checkForNextBlock();
}
}
render() {
const {
close,

View File

@ -12,6 +12,7 @@ import BackEnd from './views/backend';
import Quiz from './views/quiz';
import Modern from './views/Modern';
import { fullBlocksSelector } from '../../entities';
import {
fetchChallenge,
challengeSelector,
@ -42,11 +43,14 @@ const mapStateToProps = createSelector(
challengeSelector,
challengeMetaSelector,
paramsSelector,
fullBlocksSelector,
(
{ dashedName, isTranslated },
{ viewType, title },
params
params,
blocks
) => ({
blocks,
challenge: dashedName,
isTranslated,
params,

View File

@ -1,5 +1,4 @@
import _ from 'lodash';
import debug from 'debug';
import { Observable } from 'rx';
import { combineEpics, ofType } from 'redux-epic';
@ -10,23 +9,15 @@ import {
onRouteChallenges,
onRouteCurrentChallenge
} from './';
import { getNS as entitiesSelector } from '../../../entities';
import {
getNextChallenge,
getFirstChallengeOfNextBlock,
getFirstChallengeOfNextSuperBlock
} from '../utils';
import {
createErrorObservable,
currentChallengeSelector,
challengeSelector,
superBlocksSelector
nextChallengeSelector
} from '../../../redux';
import { langSelector } from '../../../Router/redux';
import { makeToast } from '../../../Toasts/redux';
const isDev = debug.enabled('fcc:*');
// When we change challenge, update the current challenge
// UI data.
export function challengeUpdatedEpic(actions, { getState }) {
@ -52,51 +43,14 @@ export function resetChallengeEpic(actions, { getState }) {
export function nextChallengeEpic(actions, { getState }) {
return actions::ofType(types.moveToNextChallenge)
.flatMap(() => {
let nextChallenge;
// let message = '';
// let isNewBlock = false;
// let isNewSuperBlock = false;
try {
const state = getState();
const superBlocks = superBlocksSelector(state);
const challenge = currentChallengeSelector(state);
const entities = entitiesSelector(state);
const lang = langSelector(state);
nextChallenge = getNextChallenge(challenge, entities, { isDev });
// block completed.
const { nextChallenge } = nextChallengeSelector(state);
if (!nextChallenge) {
// isNewBlock = true;
nextChallenge = getFirstChallengeOfNextBlock(
challenge,
entities,
{ isDev }
return createErrorObservable(
new Error('Next Challenge could not be found')
);
}
// superBlock completed
if (!nextChallenge) {
// isNewSuperBlock = true;
nextChallenge = getFirstChallengeOfNextSuperBlock(
challenge,
entities,
superBlocks,
{ isDev }
);
}
/* // TODO(berks): get this to work
if (isNewSuperBlock || isNewBlock) {
const getName = isNewSuperBlock ?
getCurrentSuperBlockName :
getCurrentBlockName;
const blockType = isNewSuperBlock ? 'SuperBlock' : 'Block';
message =
`You've competed the ${getName(challenge, entities)} ${blockType}!`;
}
message += ' Your next challenge has arrived.';
const toast = {
// title: isNewSuperBlock || isNewBlock ? randomVerb() : null,
message
};
*/
if (nextChallenge.isLocked) {
return Observable.of(
makeToast({
@ -116,9 +70,6 @@ export function nextChallengeEpic(actions, { getState }) {
onRouteChallenges({ lang, ...nextChallenge }),
makeToast({ message: 'Your next challenge has arrived.' })
);
} catch (err) {
return createErrorObservable(err);
}
});
}

View File

@ -77,6 +77,7 @@ export const types = createTypes([
'checkChallenge',
createAsyncTypes('submitChallenge'),
'moveToNextChallenge',
'checkForNextBlock',
// help
'openHelpModal',
@ -150,6 +151,7 @@ export const submitChallengeComplete = createAction(
);
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
export const checkForNextBlock = createAction(types.checkForNextBlock);
// help
export const openHelpModal = createAction(types.openHelpModal);

View File

@ -54,7 +54,6 @@ class InternetSettings extends PureComponent {
}
handleSubmit(values) {
console.log(values);
this.props.updateUserBackend(values);
}

View File

@ -1,12 +1,17 @@
import Fetchr from 'fetchr';
import getUserServices from '../services/user';
import getMapServices from '../services/map';
import getMapUiServices from '../services/mapUi';
import getChallengesForBlockService from '../services/challenge';
export default function bootServices(app) {
const userServices = getUserServices(app);
const mapServices = getMapServices(app);
Fetchr.registerFetcher(userServices);
Fetchr.registerFetcher(mapServices);
const challenge = getChallengesForBlockService(app);
const mapUi = getMapUiServices(app);
const user = getUserServices(app);
Fetchr.registerFetcher(challenge);
Fetchr.registerFetcher(mapUi);
Fetchr.registerFetcher(user);
app.use('/services', Fetchr.middleware());
}

View File

@ -0,0 +1,58 @@
import debug from 'debug';
import { pickBy } from 'lodash';
import { Observable } from 'rx';
import { cachedMap, getMapForLang, getChallenge } from '../utils/map';
import { shapeChallenges } from '../../common/app/redux/utils';
const log = debug('fcc:services:challenge');
const isDev = debug.enabled('fcc:*');
export default function getChallengesForBlock(app) {
const challengeMap = cachedMap(app.models);
return {
name: 'challenge',
read: function readChallengesForBlock(
req,
resource,
{ dashedName, blockName, lang = 'en' } = {},
config,
cb
) {
const getChallengeBlock$ = challengeMap.map(getMapForLang(lang))
.flatMap(({
result: { superBlocks },
entities: {
block: fullBlockMap,
challenge: challengeMap
}
}) => {
log(`sourcing challenges for the ${blockName} block`);
const requestedChallenges = pickBy(
challengeMap,
ch => ch.block === blockName
);
const entities = {
block: {
[blockName]: fullBlockMap[blockName]
},
challenge: requestedChallenges
};
const { challenge, block } = shapeChallenges(entities, isDev);
return Observable.of({
result: { superBlocks },
entities: { challenge, block }
});
});
return Observable.if(
() => !!dashedName,
getChallenge(dashedName, blockName, challengeMap, lang),
getChallengeBlock$
)
.subscribe(
result => cb(null, result),
cb
);
}
};
}

View File

@ -1,28 +0,0 @@
import { Observable } from 'rx';
import debug from 'debug';
import {
cachedMap,
getChallenge,
getMapForLang
} from '../utils/map';
const log = debug('fcc:services:map');
export default function mapService(app) {
const challengeMap = cachedMap(app.models);
return {
name: 'map',
read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
log(`${lang} language requested`);
return Observable.if(
() => !!dashedName,
getChallenge(dashedName, block, challengeMap, lang),
challengeMap.map(getMapForLang(lang))
)
.subscribe(
results => cb(null, results),
err => { log(err); cb(err); }
);
}
};
}

82
server/services/mapUi.js Normal file
View File

@ -0,0 +1,82 @@
import debug from 'debug';
import { Observable } from 'rx';
import { cachedMap, getMapForLang } from '../utils/map';
const log = debug('fcc:services:mapUi');
export default function mapUiService(app) {
const supportedLangMap = {};
const challengeMap = cachedMap(app.models);
return {
name: 'map-ui',
read: function readMapUi(req, resource, { lang = 'en' } = {}, config, cb) {
log(`generating mapUi for ${lang}`);
if (lang in supportedLangMap) {
log(`using cache for ${lang} map`);
return cb(null, supportedLangMap[lang]);
}
return challengeMap.map(getMapForLang(lang))
.flatMap(({
result: { superBlocks },
entities: {
superBlock: fullSuperBlockMap,
block: fullBlockMap,
challenge: fullChallengeMap
}
}) => {
const superBlockMap = superBlocks
.map(superBlock => fullSuperBlockMap[superBlock])
.reduce((map, { dashedName, blocks, title }) => {
map[dashedName] = { blocks, title, dashedName};
return map;
}, {});
const blockMap = Object.keys(fullBlockMap)
.map(block => fullBlockMap[block])
.reduce((map, { dashedName, title, time, challenges }) => {
map[dashedName] = { dashedName, title, time, challenges };
return map;
}, {});
const challengeMap = Object.keys(fullChallengeMap)
.map(challenge => fullChallengeMap[challenge])
.reduce((map, challenge) => {
const {
dashedName,
id,
title,
name,
block,
isLocked,
isComingSoon,
isBeta
} = challenge;
map[dashedName] = {
dashedName,
id,
title,
name,
block,
isLocked,
isComingSoon,
isBeta
};
return map;
}, {});
const mapUi = {
result: { superBlocks },
entities: {
superBlock: superBlockMap,
block: blockMap,
challenge: challengeMap
}
};
supportedLangMap[lang] = mapUi;
return Observable.of(mapUi);
}).subscribe(
mapUi => cb(null, mapUi ),
err => { log(err); return cb(err); }
);
}
};
}