Merge pull request #16771 from Bouncey/feat/splitMapService
Feat(challenge): Only send the challenges for the requested block
This commit is contained in:
@ -9,13 +9,16 @@ import ns from './ns.json';
|
|||||||
import Challenges from './Challenges.jsx';
|
import Challenges from './Challenges.jsx';
|
||||||
import {
|
import {
|
||||||
toggleThisPanel,
|
toggleThisPanel,
|
||||||
|
|
||||||
makePanelOpenSelector
|
makePanelOpenSelector
|
||||||
} from './redux';
|
} from './redux';
|
||||||
|
import { fetchNewBlock } from '../redux';
|
||||||
|
|
||||||
import { makeBlockSelector } from '../entities';
|
import { makeBlockSelector } from '../entities';
|
||||||
|
|
||||||
const dispatchActions = { toggleThisPanel };
|
const mapDispatchToProps = {
|
||||||
|
fetchNewBlock,
|
||||||
|
toggleThisPanel
|
||||||
|
};
|
||||||
function makeMapStateToProps(_, { dashedName }) {
|
function makeMapStateToProps(_, { dashedName }) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
makeBlockSelector(dashedName),
|
makeBlockSelector(dashedName),
|
||||||
@ -34,6 +37,7 @@ function makeMapStateToProps(_, { dashedName }) {
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
challenges: PropTypes.array,
|
challenges: PropTypes.array,
|
||||||
dashedName: PropTypes.string,
|
dashedName: PropTypes.string,
|
||||||
|
fetchNewBlock: PropTypes.func.isRequired,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
time: PropTypes.string,
|
time: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
@ -74,7 +78,8 @@ export class Block extends PureComponent {
|
|||||||
time,
|
time,
|
||||||
dashedName,
|
dashedName,
|
||||||
isOpen,
|
isOpen,
|
||||||
challenges
|
challenges,
|
||||||
|
fetchNewBlock
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
@ -85,6 +90,7 @@ export class Block extends PureComponent {
|
|||||||
header={ this.renderHeader(isOpen, title, time) }
|
header={ this.renderHeader(isOpen, title, time) }
|
||||||
id={ title }
|
id={ title }
|
||||||
key={ title }
|
key={ title }
|
||||||
|
onClick={ () => fetchNewBlock(dashedName) }
|
||||||
onSelect={ this.handleSelect }
|
onSelect={ this.handleSelect }
|
||||||
>
|
>
|
||||||
{ isOpen && <Challenges challenges={ challenges } /> }
|
{ isOpen && <Challenges challenges={ challenges } /> }
|
||||||
@ -96,4 +102,4 @@ export class Block extends PureComponent {
|
|||||||
Block.displayName = 'Block';
|
Block.displayName = 'Block';
|
||||||
Block.propTypes = propTypes;
|
Block.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, dispatchActions)(Block);
|
export default connect(makeMapStateToProps, mapDispatchToProps)(Block);
|
||||||
|
@ -4,23 +4,32 @@ import { connect } from 'react-redux';
|
|||||||
import { Col, Row } from 'react-bootstrap';
|
import { Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
import ns from './ns.json';
|
import ns from './ns.json';
|
||||||
|
import { Loader } from '../helperComponents';
|
||||||
import SuperBlock from './Super-Block.jsx';
|
import SuperBlock from './Super-Block.jsx';
|
||||||
import { superBlocksSelector } from '../redux';
|
import { superBlocksSelector } from '../redux';
|
||||||
|
import { fetchMapUi } from './redux';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
superBlocks: superBlocksSelector(state)
|
superBlocks: superBlocksSelector(state)
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {};
|
const mapDispatchToProps = { fetchMapUi };
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
fetchMapUi: PropTypes.func.isRequired,
|
||||||
params: PropTypes.object,
|
params: PropTypes.object,
|
||||||
superBlocks: PropTypes.array
|
superBlocks: PropTypes.array
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ShowMap extends PureComponent {
|
export class ShowMap extends PureComponent {
|
||||||
renderSuperBlocks(superBlocks) {
|
|
||||||
|
renderSuperBlocks() {
|
||||||
|
const { superBlocks } = this.props;
|
||||||
if (!Array.isArray(superBlocks) || !superBlocks.length) {
|
if (!Array.isArray(superBlocks) || !superBlocks.length) {
|
||||||
return <div>No Super Blocks</div>;
|
return (
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return superBlocks.map(dashedName => (
|
return superBlocks.map(dashedName => (
|
||||||
<SuperBlock
|
<SuperBlock
|
||||||
@ -31,12 +40,11 @@ export class ShowMap extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { superBlocks } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={ 12 }>
|
<Col xs={ 12 }>
|
||||||
<div className={ `${ns}-accordion center-block` }>
|
<div className={ `${ns}-accordion center-block` }>
|
||||||
{ this.renderSuperBlocks(superBlocks) }
|
{ this.renderSuperBlocks() }
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -27,8 +27,7 @@ function mapStateToProps(_, { dashedName }) {
|
|||||||
isOpen,
|
isOpen,
|
||||||
dashedName,
|
dashedName,
|
||||||
title: superBlock.title || dashedName,
|
title: superBlock.title || dashedName,
|
||||||
blocks: superBlock.blocks || [],
|
blocks: superBlock.blocks || []
|
||||||
message: superBlock.message
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -37,7 +36,6 @@ const propTypes = {
|
|||||||
blocks: PropTypes.array,
|
blocks: PropTypes.array,
|
||||||
dashedName: PropTypes.string,
|
dashedName: PropTypes.string,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
message: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
toggleThisPanel: PropTypes.func
|
toggleThisPanel: PropTypes.func
|
||||||
};
|
};
|
||||||
@ -52,17 +50,6 @@ export class SuperBlock extends PureComponent {
|
|||||||
this.props.toggleThisPanel(eventKey);
|
this.props.toggleThisPanel(eventKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMessage(message) {
|
|
||||||
if (!message) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={ `${ns}-block-description` }>
|
|
||||||
{ message }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHeader(isOpen, title, isCompleted) {
|
renderHeader(isOpen, title, isCompleted) {
|
||||||
return (
|
return (
|
||||||
<div className={ isCompleted ? 'faded' : '' }>
|
<div className={ isCompleted ? 'faded' : '' }>
|
||||||
@ -81,7 +68,6 @@ export class SuperBlock extends PureComponent {
|
|||||||
title,
|
title,
|
||||||
dashedName,
|
dashedName,
|
||||||
blocks,
|
blocks,
|
||||||
message,
|
|
||||||
isOpen
|
isOpen
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
@ -95,7 +81,6 @@ export class SuperBlock extends PureComponent {
|
|||||||
key={ dashedName || title }
|
key={ dashedName || title }
|
||||||
onSelect={ this.handleSelect }
|
onSelect={ this.handleSelect }
|
||||||
>
|
>
|
||||||
{ this.renderMessage(message) }
|
|
||||||
<Blocks blocks={ blocks } />
|
<Blocks blocks={ blocks } />
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
41
common/app/Map/redux/fetch-map-ui-epic.js
Normal file
41
common/app/Map/redux/fetch-map-ui-epic.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
@ -1,25 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
createAction,
|
createAction,
|
||||||
|
createAsyncTypes,
|
||||||
createTypes,
|
createTypes,
|
||||||
handleActions
|
handleActions
|
||||||
} from 'berkeleys-redux-utils';
|
} from 'berkeleys-redux-utils';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import noop from 'lodash/noop';
|
import { capitalize, noop } from 'lodash';
|
||||||
import capitalize from 'lodash/capitalize';
|
|
||||||
|
|
||||||
import * as utils from './utils.js';
|
import * as utils from './utils.js';
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
import {
|
import {
|
||||||
types as app,
|
|
||||||
createEventMetaCreator
|
createEventMetaCreator
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
|
|
||||||
export const epics = [];
|
import fetchMapUiEpic from './fetch-map-ui-epic';
|
||||||
|
|
||||||
|
export const epics = [ fetchMapUiEpic ];
|
||||||
|
|
||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
'onRouteMap',
|
'onRouteMap',
|
||||||
'initMap',
|
'initMap',
|
||||||
|
createAsyncTypes('fetchMapUi'),
|
||||||
'toggleThisPanel',
|
'toggleThisPanel',
|
||||||
|
|
||||||
'isAllCollapsed',
|
'isAllCollapsed',
|
||||||
@ -31,6 +32,9 @@ export const types = createTypes([
|
|||||||
|
|
||||||
export const initMap = createAction(types.initMap);
|
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 toggleThisPanel = createAction(types.toggleThisPanel);
|
||||||
export const collapseAll = createAction(types.collapseAll);
|
export const collapseAll = createAction(types.collapseAll);
|
||||||
|
|
||||||
@ -100,10 +104,11 @@ export default handleActions(
|
|||||||
mapUi
|
mapUi
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[app.fetchChallenges.complete]: (state, { payload }) => {
|
[types.fetchMapUi.complete]: (state, { payload }) => {
|
||||||
const { entities, result } = payload;
|
const { entities, result } = payload;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
...result,
|
||||||
mapUi: utils.createMapUi(entities, result)
|
mapUi: utils.createMapUi(entities, result)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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 uuid from 'uuid/v4';
|
||||||
import {
|
import {
|
||||||
|
combineActions,
|
||||||
composeReducers,
|
composeReducers,
|
||||||
createAction,
|
createAction,
|
||||||
createTypes,
|
createTypes,
|
||||||
@ -8,8 +9,9 @@ import {
|
|||||||
} from 'berkeleys-redux-utils';
|
} from 'berkeleys-redux-utils';
|
||||||
|
|
||||||
import { themes } from '../../utils/themes';
|
import { themes } from '../../utils/themes';
|
||||||
|
import { usernameSelector, types as app } from '../redux';
|
||||||
import { types as challenges } from '../routes/Challenges/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 ns = 'entities';
|
||||||
export const getNS = state => state[ns];
|
export const getNS = state => state[ns];
|
||||||
@ -85,7 +87,8 @@ const defaultState = {
|
|||||||
superBlock: {},
|
superBlock: {},
|
||||||
block: {},
|
block: {},
|
||||||
challenge: {},
|
challenge: {},
|
||||||
user: {}
|
user: {},
|
||||||
|
fullBlocks: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export function selectiveChallengeTitleSelector(state, dashedName) {
|
export function selectiveChallengeTitleSelector(state, dashedName) {
|
||||||
@ -148,6 +151,8 @@ export function makeSuperBlockSelector(name) {
|
|||||||
export const isChallengeLoaded = (state, { dashedName }) =>
|
export const isChallengeLoaded = (state, { dashedName }) =>
|
||||||
!!challengeMapSelector(state)[dashedName];
|
!!challengeMapSelector(state)[dashedName];
|
||||||
|
|
||||||
|
export const fullBlocksSelector = state => getNS(state).fullBlocks;
|
||||||
|
|
||||||
export default composeReducers(
|
export default composeReducers(
|
||||||
ns,
|
ns,
|
||||||
function metaReducer(state = defaultState, action) {
|
function metaReducer(state = defaultState, action) {
|
||||||
@ -163,8 +168,7 @@ export default composeReducers(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...merge(state, action.meta.entities)
|
||||||
...action.meta.entities
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -187,6 +191,18 @@ export default composeReducers(
|
|||||||
},
|
},
|
||||||
handleActions(
|
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
|
challenges.submitChallenge.complete
|
||||||
]: (state, { payload: { username, points, challengeInfo } }) => ({
|
]: (state, { payload: { username, points, challengeInfo } }) => ({
|
||||||
|
@ -9,9 +9,15 @@ import {
|
|||||||
delayedRedirect,
|
delayedRedirect,
|
||||||
|
|
||||||
fetchChallengeCompleted,
|
fetchChallengeCompleted,
|
||||||
fetchChallengesCompleted
|
fetchChallengesCompleted,
|
||||||
|
fetchNewBlock,
|
||||||
|
challengeSelector,
|
||||||
|
nextChallengeSelector
|
||||||
} from './';
|
} from './';
|
||||||
import { isChallengeLoaded } from '../entities/index.js';
|
import {
|
||||||
|
isChallengeLoaded,
|
||||||
|
fullBlocksSelector
|
||||||
|
} from '../entities';
|
||||||
|
|
||||||
import { shapeChallenges } from './utils';
|
import { shapeChallenges } from './utils';
|
||||||
import { types as challenge } from '../routes/Challenges/redux';
|
import { types as challenge } from '../routes/Challenges/redux';
|
||||||
@ -19,12 +25,12 @@ import { langSelector } from '../Router/redux';
|
|||||||
|
|
||||||
const isDev = debug.enabled('fcc:*');
|
const isDev = debug.enabled('fcc:*');
|
||||||
|
|
||||||
export function fetchChallengeEpic(actions, { getState }, { services }) {
|
function fetchChallengeEpic(actions, { getState }, { services }) {
|
||||||
return actions::ofType(challenge.onRouteChallenges)
|
return actions::ofType(challenge.onRouteChallenges)
|
||||||
.filter(({ payload }) => !isChallengeLoaded(getState(), payload))
|
.filter(({ payload }) => !isChallengeLoaded(getState(), payload))
|
||||||
.flatMapLatest(({ payload: params }) => {
|
.flatMapLatest(({ payload: params }) => {
|
||||||
const options = {
|
const options = {
|
||||||
service: 'map',
|
service: 'challenge',
|
||||||
params
|
params
|
||||||
};
|
};
|
||||||
return services.readService$(options)
|
return services.readService$(options)
|
||||||
@ -49,39 +55,65 @@ export function fetchChallengeEpic(actions, { getState }, { services }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchChallengesEpic(
|
export function fetchChallengesForBlockEpic(
|
||||||
actions,
|
actions,
|
||||||
{ getState },
|
{ getState },
|
||||||
{ services }
|
{ services }
|
||||||
) {
|
) {
|
||||||
return actions::ofType(
|
return actions::ofType(
|
||||||
types.appMounted,
|
types.appMounted,
|
||||||
types.updateChallenges
|
types.updateChallenges,
|
||||||
|
types.fetchNewBlock.start
|
||||||
)
|
)
|
||||||
.flatMapLatest(() => {
|
.flatMapLatest(({ type, payload }) => {
|
||||||
const lang = langSelector(getState());
|
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 = {
|
const options = {
|
||||||
params: { lang },
|
params: { lang, blockName },
|
||||||
service: 'map'
|
service: 'challenge'
|
||||||
};
|
};
|
||||||
return services.readService$(options)
|
return services.readService$(options)
|
||||||
.retry(3)
|
.retry(3)
|
||||||
.map(({ entities, ...res }) => ({
|
.map(fetchChallengesCompleted)
|
||||||
entities: shapeChallenges(
|
|
||||||
entities,
|
|
||||||
isDev
|
|
||||||
),
|
|
||||||
...res
|
|
||||||
}))
|
|
||||||
.map(({ entities, result } = {}) => {
|
|
||||||
return fetchChallengesCompleted(
|
|
||||||
entities,
|
|
||||||
result
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.startWith({ type: types.fetchChallenges.start })
|
.startWith({ type: types.fetchChallenges.start })
|
||||||
.catch(createErrorObservable);
|
.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
|
||||||
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import { flow, identity } from 'lodash';
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import {
|
import {
|
||||||
combineActions,
|
combineActions,
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
handleActions
|
handleActions
|
||||||
} from 'berkeleys-redux-utils';
|
} from 'berkeleys-redux-utils';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
import fetchUserEpic from './fetch-user-epic.js';
|
import fetchUserEpic from './fetch-user-epic.js';
|
||||||
import updateMyCurrentChallengeEpic from './update-my-challenge-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 { utils } from '../Flash/redux';
|
||||||
import { paramsSelector } from '../Router/redux';
|
import { paramsSelector } from '../Router/redux';
|
||||||
import { types as challenges } from '../routes/Challenges/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 ns from '../ns.json';
|
||||||
|
|
||||||
import { themes, invertTheme } from '../../utils/themes.js';
|
import { themes, invertTheme } from '../../utils/themes.js';
|
||||||
|
|
||||||
|
const isDev = debug.enabled('fcc:*');
|
||||||
|
|
||||||
export const epics = [
|
export const epics = [
|
||||||
fetchChallengesEpic,
|
fetchChallengesEpic,
|
||||||
fetchUserEpic,
|
fetchUserEpic,
|
||||||
@ -41,6 +50,7 @@ export const types = createTypes([
|
|||||||
|
|
||||||
createAsyncTypes('fetchChallenge'),
|
createAsyncTypes('fetchChallenge'),
|
||||||
createAsyncTypes('fetchChallenges'),
|
createAsyncTypes('fetchChallenges'),
|
||||||
|
createAsyncTypes('fetchNewBlock'),
|
||||||
'updateChallenges',
|
'updateChallenges',
|
||||||
createAsyncTypes('fetchOtherUser'),
|
createAsyncTypes('fetchOtherUser'),
|
||||||
createAsyncTypes('fetchUser'),
|
createAsyncTypes('fetchUser'),
|
||||||
@ -66,7 +76,7 @@ const throwIfUndefined = () => {
|
|||||||
// label?: String,
|
// label?: String,
|
||||||
// value?: Number
|
// value?: Number
|
||||||
// }) => () => Object
|
// }) => () => Object
|
||||||
export const createEventMetaCreator = ({
|
export function createEventMetaCreator({
|
||||||
// categories are features or namespaces of the app (capitalized):
|
// categories are features or namespaces of the app (capitalized):
|
||||||
// Map, Nav, Challenges, and so on
|
// Map, Nav, Challenges, and so on
|
||||||
category = throwIfUndefined,
|
category = throwIfUndefined,
|
||||||
@ -80,15 +90,17 @@ export const createEventMetaCreator = ({
|
|||||||
label,
|
label,
|
||||||
// used to tack some specific value for a GA event
|
// used to tack some specific value for a GA event
|
||||||
value
|
value
|
||||||
} = throwIfUndefined) => () => ({
|
} = throwIfUndefined) {
|
||||||
analytics: {
|
return () => ({
|
||||||
type: 'event',
|
analytics: {
|
||||||
category,
|
type: 'event',
|
||||||
action,
|
category,
|
||||||
label,
|
action,
|
||||||
value
|
label,
|
||||||
}
|
value
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const onRouteHome = createAction(types.onRouteHome);
|
export const onRouteHome = createAction(types.onRouteHome);
|
||||||
export const appMounted = createAction(types.appMounted);
|
export const appMounted = createAction(types.appMounted);
|
||||||
@ -101,15 +113,20 @@ export const fetchChallengeCompleted = createAction(
|
|||||||
null,
|
null,
|
||||||
meta => ({
|
meta => ({
|
||||||
...meta,
|
...meta,
|
||||||
..._.flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
|
...flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
export const fetchChallenges = createAction('' + types.fetchChallenges);
|
export const fetchChallenges = createAction('' + types.fetchChallenges);
|
||||||
export const fetchChallengesCompleted = createAction(
|
export const fetchChallengesCompleted = createAction(
|
||||||
types.fetchChallenges.complete,
|
types.fetchChallenges.complete
|
||||||
(entities, result) => ({ entities, result }),
|
|
||||||
entities => ({ entities })
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchNewBlock = createAction(types.fetchNewBlock.start);
|
||||||
|
export const fetchNewBlockComplete = createAction(
|
||||||
|
types.fetchNewBlock.complete,
|
||||||
|
({ entities }) => entities
|
||||||
|
);
|
||||||
|
|
||||||
export const updateChallenges = createAction(types.updateChallenges);
|
export const updateChallenges = createAction(types.updateChallenges);
|
||||||
|
|
||||||
// updateTitle(title: String) => Action
|
// updateTitle(title: String) => Action
|
||||||
@ -122,7 +139,7 @@ export const fetchOtherUser = createAction(types.fetchOtherUser.start);
|
|||||||
export const fetchOtherUserComplete = createAction(
|
export const fetchOtherUserComplete = createAction(
|
||||||
types.fetchOtherUser.complete,
|
types.fetchOtherUser.complete,
|
||||||
({ result }) => result,
|
({ result }) => result,
|
||||||
_.identity
|
identity
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetchUser() => Action
|
// fetchUser() => Action
|
||||||
@ -131,7 +148,7 @@ export const fetchUser = createAction(types.fetchUser);
|
|||||||
export const fetchUserComplete = createAction(
|
export const fetchUserComplete = createAction(
|
||||||
types.fetchUser.complete,
|
types.fetchUser.complete,
|
||||||
({ result }) => result,
|
({ result }) => result,
|
||||||
_.identity
|
identity
|
||||||
);
|
);
|
||||||
|
|
||||||
export const showSignIn = createAction(types.showSignIn);
|
export const showSignIn = createAction(types.showSignIn);
|
||||||
@ -209,7 +226,7 @@ export const userByNameSelector = state => {
|
|||||||
return userMap[username] || {};
|
return userMap[username] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeSelector = _.flow(
|
export const themeSelector = flow(
|
||||||
userSelector,
|
userSelector,
|
||||||
user => user.theme || themes.default
|
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(
|
export default handleActions(
|
||||||
() => ({
|
() => ({
|
||||||
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
||||||
@ -275,7 +326,7 @@ export default handleActions(
|
|||||||
}),
|
}),
|
||||||
[combineActions(
|
[combineActions(
|
||||||
types.fetchChallenge.complete,
|
types.fetchChallenge.complete,
|
||||||
types.fetchChallenges.complete
|
map.fetchMapUi.complete
|
||||||
)]: (state, { payload }) => ({
|
)]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
superBlocks: payload.result.superBlocks
|
superBlocks: payload.result.superBlocks
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import flowRight from 'lodash/flowRight';
|
import flowRight from 'lodash/flowRight';
|
||||||
import { createNameIdMap } from '../../utils/map.js';
|
import { createNameIdMap } from '../../utils/map.js';
|
||||||
|
import { partial } from 'lodash';
|
||||||
|
|
||||||
export function filterComingSoonBetaChallenge(
|
export function filterComingSoonBetaChallenge(
|
||||||
isDev = false,
|
isDev = false,
|
||||||
@ -13,7 +14,7 @@ export function filterComingSoonBetaFromEntities(
|
|||||||
{ challenge: challengeMap, block: blockMap = {}, ...rest },
|
{ challenge: challengeMap, block: blockMap = {}, ...rest },
|
||||||
isDev = false
|
isDev = false
|
||||||
) {
|
) {
|
||||||
const filter = filterComingSoonBetaChallenge.bind(null, isDev);
|
const filter = partial(filterComingSoonBetaChallenge, isDev);
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
block: Object.keys(blockMap)
|
block: Object.keys(blockMap)
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
closeChallengeModal,
|
closeChallengeModal,
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
|
|
||||||
|
checkForNextBlock,
|
||||||
|
|
||||||
challengeModalSelector,
|
challengeModalSelector,
|
||||||
successMessageSelector
|
successMessageSelector
|
||||||
} from './redux';
|
} from './redux';
|
||||||
@ -34,12 +36,14 @@ const mapDispatchToProps = function(dispatch) {
|
|||||||
},
|
},
|
||||||
submitChallenge: () => {
|
submitChallenge: () => {
|
||||||
dispatch(submitChallenge());
|
dispatch(submitChallenge());
|
||||||
}
|
},
|
||||||
|
checkForNextBlock: () => dispatch(checkForNextBlock())
|
||||||
};
|
};
|
||||||
return () => dispatchers;
|
return () => dispatchers;
|
||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
checkForNextBlock: PropTypes.func.isRequired,
|
||||||
close: PropTypes.func.isRequired,
|
close: PropTypes.func.isRequired,
|
||||||
handleKeypress: PropTypes.func.isRequired,
|
handleKeypress: PropTypes.func.isRequired,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
@ -48,6 +52,11 @@ const propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class CompletionModal extends PureComponent {
|
export class CompletionModal extends PureComponent {
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.props.isOpen) {
|
||||||
|
this.props.checkForNextBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
close,
|
close,
|
||||||
|
@ -12,6 +12,7 @@ import BackEnd from './views/backend';
|
|||||||
import Quiz from './views/quiz';
|
import Quiz from './views/quiz';
|
||||||
import Modern from './views/Modern';
|
import Modern from './views/Modern';
|
||||||
|
|
||||||
|
import { fullBlocksSelector } from '../../entities';
|
||||||
import {
|
import {
|
||||||
fetchChallenge,
|
fetchChallenge,
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
@ -42,11 +43,14 @@ const mapStateToProps = createSelector(
|
|||||||
challengeSelector,
|
challengeSelector,
|
||||||
challengeMetaSelector,
|
challengeMetaSelector,
|
||||||
paramsSelector,
|
paramsSelector,
|
||||||
|
fullBlocksSelector,
|
||||||
(
|
(
|
||||||
{ dashedName, isTranslated },
|
{ dashedName, isTranslated },
|
||||||
{ viewType, title },
|
{ viewType, title },
|
||||||
params
|
params,
|
||||||
|
blocks
|
||||||
) => ({
|
) => ({
|
||||||
|
blocks,
|
||||||
challenge: dashedName,
|
challenge: dashedName,
|
||||||
isTranslated,
|
isTranslated,
|
||||||
params,
|
params,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import debug from 'debug';
|
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { combineEpics, ofType } from 'redux-epic';
|
import { combineEpics, ofType } from 'redux-epic';
|
||||||
|
|
||||||
@ -10,23 +9,15 @@ import {
|
|||||||
onRouteChallenges,
|
onRouteChallenges,
|
||||||
onRouteCurrentChallenge
|
onRouteCurrentChallenge
|
||||||
} from './';
|
} from './';
|
||||||
import { getNS as entitiesSelector } from '../../../entities';
|
|
||||||
import {
|
|
||||||
getNextChallenge,
|
|
||||||
getFirstChallengeOfNextBlock,
|
|
||||||
getFirstChallengeOfNextSuperBlock
|
|
||||||
} from '../utils';
|
|
||||||
import {
|
import {
|
||||||
createErrorObservable,
|
createErrorObservable,
|
||||||
currentChallengeSelector,
|
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
superBlocksSelector
|
nextChallengeSelector
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
import { langSelector } from '../../../Router/redux';
|
import { langSelector } from '../../../Router/redux';
|
||||||
import { makeToast } from '../../../Toasts/redux';
|
import { makeToast } from '../../../Toasts/redux';
|
||||||
|
|
||||||
const isDev = debug.enabled('fcc:*');
|
|
||||||
|
|
||||||
// When we change challenge, update the current challenge
|
// When we change challenge, update the current challenge
|
||||||
// UI data.
|
// UI data.
|
||||||
export function challengeUpdatedEpic(actions, { getState }) {
|
export function challengeUpdatedEpic(actions, { getState }) {
|
||||||
@ -52,73 +43,33 @@ export function resetChallengeEpic(actions, { getState }) {
|
|||||||
export function nextChallengeEpic(actions, { getState }) {
|
export function nextChallengeEpic(actions, { getState }) {
|
||||||
return actions::ofType(types.moveToNextChallenge)
|
return actions::ofType(types.moveToNextChallenge)
|
||||||
.flatMap(() => {
|
.flatMap(() => {
|
||||||
let nextChallenge;
|
const state = getState();
|
||||||
// let message = '';
|
const lang = langSelector(state);
|
||||||
// let isNewBlock = false;
|
const { nextChallenge } = nextChallengeSelector(state);
|
||||||
// let isNewSuperBlock = false;
|
if (!nextChallenge) {
|
||||||
try {
|
return createErrorObservable(
|
||||||
const state = getState();
|
new Error('Next Challenge could not be found')
|
||||||
const superBlocks = superBlocksSelector(state);
|
|
||||||
const challenge = currentChallengeSelector(state);
|
|
||||||
const entities = entitiesSelector(state);
|
|
||||||
const lang = langSelector(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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/* // 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({
|
|
||||||
message: 'The next challenge has not been unlocked. ' +
|
|
||||||
'Please revisit the required (*) challenges ' +
|
|
||||||
'that have not been passed yet. ',
|
|
||||||
timeout: 15000
|
|
||||||
}),
|
|
||||||
onRouteCurrentChallenge()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Observable.of(
|
|
||||||
// normally we wouldn't need to add the lang as
|
|
||||||
// addLangToRoutesEnhancer should add langs for us, but the way
|
|
||||||
// enhancers/middlewares and RFR orders things this action will not
|
|
||||||
// see addLangToRoutesEnhancer and cause RFR to render NotFound
|
|
||||||
onRouteChallenges({ lang, ...nextChallenge }),
|
|
||||||
makeToast({ message: 'Your next challenge has arrived.' })
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
return createErrorObservable(err);
|
|
||||||
}
|
}
|
||||||
|
if (nextChallenge.isLocked) {
|
||||||
|
return Observable.of(
|
||||||
|
makeToast({
|
||||||
|
message: 'The next challenge has not been unlocked. ' +
|
||||||
|
'Please revisit the required (*) challenges ' +
|
||||||
|
'that have not been passed yet. ',
|
||||||
|
timeout: 15000
|
||||||
|
}),
|
||||||
|
onRouteCurrentChallenge()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Observable.of(
|
||||||
|
// normally we wouldn't need to add the lang as
|
||||||
|
// addLangToRoutesEnhancer should add langs for us, but the way
|
||||||
|
// enhancers/middlewares and RFR orders things this action will not
|
||||||
|
// see addLangToRoutesEnhancer and cause RFR to render NotFound
|
||||||
|
onRouteChallenges({ lang, ...nextChallenge }),
|
||||||
|
makeToast({ message: 'Your next challenge has arrived.' })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +77,7 @@ export const types = createTypes([
|
|||||||
'checkChallenge',
|
'checkChallenge',
|
||||||
createAsyncTypes('submitChallenge'),
|
createAsyncTypes('submitChallenge'),
|
||||||
'moveToNextChallenge',
|
'moveToNextChallenge',
|
||||||
|
'checkForNextBlock',
|
||||||
|
|
||||||
// help
|
// help
|
||||||
'openHelpModal',
|
'openHelpModal',
|
||||||
@ -150,6 +151,7 @@ export const submitChallengeComplete = createAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
||||||
|
export const checkForNextBlock = createAction(types.checkForNextBlock);
|
||||||
|
|
||||||
// help
|
// help
|
||||||
export const openHelpModal = createAction(types.openHelpModal);
|
export const openHelpModal = createAction(types.openHelpModal);
|
||||||
|
@ -54,7 +54,6 @@ class InternetSettings extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit(values) {
|
handleSubmit(values) {
|
||||||
console.log(values);
|
|
||||||
this.props.updateUserBackend(values);
|
this.props.updateUserBackend(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import Fetchr from 'fetchr';
|
import Fetchr from 'fetchr';
|
||||||
import getUserServices from '../services/user';
|
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) {
|
export default function bootServices(app) {
|
||||||
const userServices = getUserServices(app);
|
|
||||||
const mapServices = getMapServices(app);
|
|
||||||
|
|
||||||
Fetchr.registerFetcher(userServices);
|
const challenge = getChallengesForBlockService(app);
|
||||||
Fetchr.registerFetcher(mapServices);
|
const mapUi = getMapUiServices(app);
|
||||||
|
const user = getUserServices(app);
|
||||||
|
|
||||||
|
Fetchr.registerFetcher(challenge);
|
||||||
|
Fetchr.registerFetcher(mapUi);
|
||||||
|
Fetchr.registerFetcher(user);
|
||||||
|
|
||||||
app.use('/services', Fetchr.middleware());
|
app.use('/services', Fetchr.middleware());
|
||||||
}
|
}
|
||||||
|
58
server/services/challenge.js
Normal file
58
server/services/challenge.js
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -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
82
server/services/mapUi.js
Normal 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); }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user