feat(challenge): Initial build of the challenge service
This commit is contained in:
committed by
Stuart Taylor
parent
d17c2d33eb
commit
a7587ed6f0
@ -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={{ hieght: '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>
|
||||||
|
@ -17,7 +17,8 @@ export default function fetchMapUiEpic(
|
|||||||
{ services }
|
{ services }
|
||||||
) {
|
) {
|
||||||
return actions::ofType(
|
return actions::ofType(
|
||||||
appTypes.appMounted
|
appTypes.appMounted,
|
||||||
|
types.fetchMapUi.start
|
||||||
)
|
)
|
||||||
.flatMapLatest(() => {
|
.flatMapLatest(() => {
|
||||||
const lang = langSelector(getState());
|
const lang = langSelector(getState());
|
||||||
@ -34,13 +35,7 @@ export default function fetchMapUiEpic(
|
|||||||
),
|
),
|
||||||
...res
|
...res
|
||||||
}))
|
}))
|
||||||
.map(({ entities, result } = {}) => {
|
.map(fetchMapUiComplete)
|
||||||
return fetchMapUiComplete(
|
|
||||||
entities,
|
|
||||||
result
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.startWith({ type: types.fetchMapUi.start })
|
|
||||||
.catch(createErrorObservable);
|
.catch(createErrorObservable);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -32,11 +32,8 @@ export const types = createTypes([
|
|||||||
|
|
||||||
export const initMap = createAction(types.initMap);
|
export const initMap = createAction(types.initMap);
|
||||||
|
|
||||||
export const fetchMapUiComplete = createAction(
|
export const fetchMapUi = createAction(types.fetchMapUi.start);
|
||||||
types.fetchMapUi.complete,
|
export const fetchMapUiComplete = createAction(types.fetchMapUi.complete);
|
||||||
(entities, result) => ({ entities, result }),
|
|
||||||
entities => ({ entities })
|
|
||||||
);
|
|
||||||
|
|
||||||
export const toggleThisPanel = createAction(types.toggleThisPanel);
|
export const toggleThisPanel = createAction(types.toggleThisPanel);
|
||||||
export const collapseAll = createAction(types.collapseAll);
|
export const collapseAll = createAction(types.collapseAll);
|
||||||
@ -111,6 +108,7 @@ export default handleActions(
|
|||||||
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, merge } 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) {
|
||||||
@ -162,7 +167,9 @@ export default composeReducers(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return merge(state, action.meta.entities);
|
return {
|
||||||
|
...merge(state, action.meta.entities)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
@ -184,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,10 @@ import {
|
|||||||
delayedRedirect,
|
delayedRedirect,
|
||||||
|
|
||||||
fetchChallengeCompleted,
|
fetchChallengeCompleted,
|
||||||
fetchChallengesCompleted
|
fetchChallengesCompleted,
|
||||||
|
challengeSelector
|
||||||
} from './';
|
} from './';
|
||||||
import { isChallengeLoaded } from '../entities/index.js';
|
import { isChallengeLoaded, fullBlocksSelector } from '../entities/index.js';
|
||||||
|
|
||||||
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,7 +20,7 @@ import { langSelector } from '../Router/redux';
|
|||||||
|
|
||||||
const isDev = debug.enabled('fcc:*');
|
const isDev = debug.enabled('fcc:*');
|
||||||
|
|
||||||
export default 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 }) => {
|
||||||
@ -49,38 +50,40 @@ export default 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({ type: 'NULL'});
|
||||||
|
}
|
||||||
|
blockName = payload;
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
params: { lang },
|
params: { lang, blockName },
|
||||||
service: 'map'
|
service: 'challenges-for-block'
|
||||||
};
|
};
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default combineEpics(fetchChallengeEpic, fetchChallengesForBlockEpic);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import { flow, identity } from 'lodash';
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import {
|
import {
|
||||||
combineActions,
|
combineActions,
|
||||||
@ -19,6 +19,7 @@ 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 { types as map } from '../Map/redux';
|
||||||
import { challengeToFiles } from '../routes/Challenges/utils';
|
import { challengeToFiles } from '../routes/Challenges/utils';
|
||||||
|
|
||||||
import ns from '../ns.json';
|
import ns from '../ns.json';
|
||||||
@ -41,6 +42,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 +68,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,7 +82,8 @@ 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) {
|
||||||
|
return () => ({
|
||||||
analytics: {
|
analytics: {
|
||||||
type: 'event',
|
type: 'event',
|
||||||
category,
|
category,
|
||||||
@ -89,6 +92,7 @@ export const createEventMetaCreator = ({
|
|||||||
value
|
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 +105,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 +131,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 +140,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 +218,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
|
||||||
);
|
);
|
||||||
@ -275,11 +284,13 @@ export default handleActions(
|
|||||||
}),
|
}),
|
||||||
[combineActions(
|
[combineActions(
|
||||||
types.fetchChallenge.complete,
|
types.fetchChallenge.complete,
|
||||||
types.fetchChallenges.complete
|
map.fetchMapUi.complete
|
||||||
)]: (state, { payload }) => ({
|
)]: (state, { payload }) => {
|
||||||
|
return ({
|
||||||
...state,
|
...state,
|
||||||
superBlocks: payload.result.superBlocks
|
superBlocks: payload.result.superBlocks
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
[challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({
|
[challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({
|
||||||
...state,
|
...state,
|
||||||
currentChallenge: dashedName
|
currentChallenge: dashedName
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -54,7 +54,6 @@ class InternetSettings extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit(values) {
|
handleSubmit(values) {
|
||||||
console.log(values);
|
|
||||||
this.props.updateUserBackend(values);
|
this.props.updateUserBackend(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,14 +2,19 @@ import Fetchr from 'fetchr';
|
|||||||
import getUserServices from '../services/user';
|
import getUserServices from '../services/user';
|
||||||
import getMapServices from '../services/map';
|
import getMapServices from '../services/map';
|
||||||
import getMapUiServices from '../services/mapUi';
|
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);
|
|
||||||
const mapUiServices = getMapUiServices(app);
|
|
||||||
|
|
||||||
Fetchr.registerFetcher(userServices);
|
const user = getUserServices(app);
|
||||||
Fetchr.registerFetcher(mapServices);
|
const map = getMapServices(app);
|
||||||
Fetchr.registerFetcher(mapUiServices);
|
const mapUi = getMapUiServices(app);
|
||||||
|
const challenge = getChallengesForBlockService(app);
|
||||||
|
|
||||||
|
Fetchr.registerFetcher(user);
|
||||||
|
Fetchr.registerFetcher(map);
|
||||||
|
Fetchr.registerFetcher(mapUi);
|
||||||
|
Fetchr.registerFetcher(challenge);
|
||||||
|
|
||||||
app.use('/services', Fetchr.middleware());
|
app.use('/services', Fetchr.middleware());
|
||||||
}
|
}
|
||||||
|
53
server/services/challenge.js
Normal file
53
server/services/challenge.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { pickBy } from 'lodash';
|
||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
import { cachedMap, getMapForLang } 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: 'challenges-for-block',
|
||||||
|
read: function readChallengesForBlock(
|
||||||
|
req,
|
||||||
|
resource,
|
||||||
|
{ blockName, lang = 'en' } = {},
|
||||||
|
config,
|
||||||
|
cb
|
||||||
|
) {
|
||||||
|
log(`sourcing challenges for the ${blockName} block`);
|
||||||
|
return challengeMap.map(getMapForLang(lang))
|
||||||
|
.flatMap(({
|
||||||
|
result: { superBlocks },
|
||||||
|
entities: {
|
||||||
|
block: fullBlockMap,
|
||||||
|
challenge: challengeMap
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
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 }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
result => cb(null, result),
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -5,12 +5,18 @@ import { cachedMap, getMapForLang } from '../utils/map';
|
|||||||
|
|
||||||
const log = debug('fcc:services:mapUi');
|
const log = debug('fcc:services:mapUi');
|
||||||
|
|
||||||
|
|
||||||
export default function mapUiService(app) {
|
export default function mapUiService(app) {
|
||||||
|
const supportedLangMap = {};
|
||||||
const challengeMap = cachedMap(app.models);
|
const challengeMap = cachedMap(app.models);
|
||||||
return {
|
return {
|
||||||
name: 'map-ui',
|
name: 'map-ui',
|
||||||
read: function readMapUi(req, resource, { lang = 'en' } = {}, config, cb) {
|
read: function readMapUi(req, resource, { lang = 'en' } = {}, config, cb) {
|
||||||
log(`generating mapUi for ${lang}`);
|
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))
|
return challengeMap.map(getMapForLang(lang))
|
||||||
.flatMap(({
|
.flatMap(({
|
||||||
result: { superBlocks },
|
result: { superBlocks },
|
||||||
@ -34,19 +40,39 @@ export default function mapUiService(app) {
|
|||||||
}, {});
|
}, {});
|
||||||
const challengeMap = Object.keys(fullChallengeMap)
|
const challengeMap = Object.keys(fullChallengeMap)
|
||||||
.map(challenge => fullChallengeMap[challenge])
|
.map(challenge => fullChallengeMap[challenge])
|
||||||
.reduce((map, { dashedName, name, id}) => {
|
.reduce((map, challenge) => {
|
||||||
map[dashedName] = {name, dashedName, id};
|
const {
|
||||||
|
dashedName,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
name,
|
||||||
|
block,
|
||||||
|
isLocked,
|
||||||
|
isComingSoon,
|
||||||
|
isBeta
|
||||||
|
} = challenge;
|
||||||
|
map[dashedName] = {
|
||||||
|
dashedName,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
name,
|
||||||
|
block,
|
||||||
|
isLocked,
|
||||||
|
isComingSoon,
|
||||||
|
isBeta
|
||||||
|
};
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
const mapUi = {
|
||||||
return Observable.of({
|
|
||||||
result: { superBlocks },
|
result: { superBlocks },
|
||||||
entities: {
|
entities: {
|
||||||
superBlock: superBlockMap,
|
superBlock: superBlockMap,
|
||||||
block: blockMap,
|
block: blockMap,
|
||||||
challenge: challengeMap
|
challenge: challengeMap
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
supportedLangMap[lang] = mapUi;
|
||||||
|
return Observable.of(mapUi);
|
||||||
}).subscribe(
|
}).subscribe(
|
||||||
mapUi => cb(null, mapUi ),
|
mapUi => cb(null, mapUi ),
|
||||||
err => { log(err); return cb(err); }
|
err => { log(err); return cb(err); }
|
||||||
|
@ -154,6 +154,58 @@ export function getMapForLang(lang) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateMapForLang(
|
||||||
|
superBlocks,
|
||||||
|
fullSuperBlockMap,
|
||||||
|
fullBlockMap,
|
||||||
|
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,
|
||||||
|
block,
|
||||||
|
isLocked,
|
||||||
|
isComingSoon,
|
||||||
|
isBeta
|
||||||
|
} = challenge;
|
||||||
|
map[dashedName] = {
|
||||||
|
dashedName,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
block,
|
||||||
|
isLocked,
|
||||||
|
isComingSoon,
|
||||||
|
isBeta
|
||||||
|
};
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: { superBlocks },
|
||||||
|
entities: {
|
||||||
|
superBlock: superBlockMap,
|
||||||
|
block: blockMap,
|
||||||
|
challenge: challengeMap
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// type ObjectId: String;
|
// type ObjectId: String;
|
||||||
// getChallengeById(
|
// getChallengeById(
|
||||||
// map: Observable[map],
|
// map: Observable[map],
|
||||||
|
Reference in New Issue
Block a user