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 {
|
||||
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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
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 {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
@ -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 } }) => ({
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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,
|
||||
@ -88,7 +99,8 @@ export const createEventMetaCreator = ({
|
||||
label,
|
||||
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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -54,7 +54,6 @@ class InternetSettings extends PureComponent {
|
||||
}
|
||||
|
||||
handleSubmit(values) {
|
||||
console.log(values);
|
||||
this.props.updateUserBackend(values);
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
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