feat(challenge): Initial build of the challenge service

This commit is contained in:
Stuart Taylor
2018-02-23 12:20:13 +00:00
committed by Stuart Taylor
parent d17c2d33eb
commit a7587ed6f0
14 changed files with 263 additions and 83 deletions

View File

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

View File

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

View File

@@ -17,7 +17,8 @@ export default function fetchMapUiEpic(
{ services }
) {
return actions::ofType(
appTypes.appMounted
appTypes.appMounted,
types.fetchMapUi.start
)
.flatMapLatest(() => {
const lang = langSelector(getState());
@@ -34,13 +35,7 @@ export default function fetchMapUiEpic(
),
...res
}))
.map(({ entities, result } = {}) => {
return fetchMapUiComplete(
entities,
result
);
})
.startWith({ type: types.fetchMapUi.start })
.map(fetchMapUiComplete)
.catch(createErrorObservable);
});
}

View File

@@ -32,11 +32,8 @@ export const types = createTypes([
export const initMap = createAction(types.initMap);
export const fetchMapUiComplete = createAction(
types.fetchMapUi.complete,
(entities, result) => ({ entities, result }),
entities => ({ entities })
);
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);
@@ -111,6 +108,7 @@ export default handleActions(
const { entities, result } = payload;
return {
...state,
...result,
mapUi: utils.createMapUi(entities, result)
};
}

View File

@@ -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 {
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) {
@@ -162,7 +167,9 @@ export default composeReducers(
}
};
}
return merge(state, action.meta.entities);
return {
...merge(state, action.meta.entities)
};
}
return state;
},
@@ -184,6 +191,18 @@ export default composeReducers(
},
handleActions(
() => ({
[
combineActions(
app.fetchChallenges.complete,
map.fetchMapUi.complete
)
]: (state, { payload }) => {
const {entities: { block } } = payload;
return {
...merge(state, payload.entities),
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
};
},
[
challenges.submitChallenge.complete
]: (state, { payload: { username, points, challengeInfo } }) => ({

View File

@@ -9,9 +9,10 @@ import {
delayedRedirect,
fetchChallengeCompleted,
fetchChallengesCompleted
fetchChallengesCompleted,
challengeSelector
} from './';
import { isChallengeLoaded } from '../entities/index.js';
import { isChallengeLoaded, fullBlocksSelector } from '../entities/index.js';
import { shapeChallenges } from './utils';
import { types as challenge } from '../routes/Challenges/redux';
@@ -19,7 +20,7 @@ import { langSelector } from '../Router/redux';
const isDev = debug.enabled('fcc:*');
export default function fetchChallengeEpic(actions, { getState }, { services }) {
function fetchChallengeEpic(actions, { getState }, { services }) {
return actions::ofType(challenge.onRouteChallenges)
.filter(({ payload }) => !isChallengeLoaded(getState(), payload))
.flatMapLatest(({ payload: params }) => {
@@ -49,38 +50,40 @@ export default 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({ type: 'NULL'});
}
blockName = payload;
}
const options = {
params: { lang },
service: 'map'
params: { lang, blockName },
service: 'challenges-for-block'
};
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);
});
}
export default combineEpics(fetchChallengeEpic, fetchChallengesForBlockEpic);

View File

@@ -1,4 +1,4 @@
import _ from 'lodash';
import { flow, identity } from 'lodash';
import { Observable } from 'rx';
import {
combineActions,
@@ -19,6 +19,7 @@ import { updateThemeMetacreator, entitiesSelector } from '../entities';
import { utils } from '../Flash/redux';
import { paramsSelector } from '../Router/redux';
import { types as challenges } from '../routes/Challenges/redux';
import { types as map } from '../Map/redux';
import { challengeToFiles } from '../routes/Challenges/utils';
import ns from '../ns.json';
@@ -41,6 +42,7 @@ export const types = createTypes([
createAsyncTypes('fetchChallenge'),
createAsyncTypes('fetchChallenges'),
createAsyncTypes('fetchNewBlock'),
'updateChallenges',
createAsyncTypes('fetchOtherUser'),
createAsyncTypes('fetchUser'),
@@ -66,7 +68,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,15 +82,17 @@ export const createEventMetaCreator = ({
label,
// used to tack some specific value for a GA event
value
} = throwIfUndefined) => () => ({
analytics: {
type: 'event',
category,
action,
label,
value
}
});
} = throwIfUndefined) {
return () => ({
analytics: {
type: 'event',
category,
action,
label,
value
}
});
}
export const onRouteHome = createAction(types.onRouteHome);
export const appMounted = createAction(types.appMounted);
@@ -101,15 +105,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 +131,7 @@ export const fetchOtherUser = createAction(types.fetchOtherUser.start);
export const fetchOtherUserComplete = createAction(
types.fetchOtherUser.complete,
({ result }) => result,
_.identity
identity
);
// fetchUser() => Action
@@ -131,7 +140,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 +218,7 @@ export const userByNameSelector = state => {
return userMap[username] || {};
};
export const themeSelector = _.flow(
export const themeSelector = flow(
userSelector,
user => user.theme || themes.default
);
@@ -275,11 +284,13 @@ export default handleActions(
}),
[combineActions(
types.fetchChallenge.complete,
types.fetchChallenges.complete
)]: (state, { payload }) => ({
map.fetchMapUi.complete
)]: (state, { payload }) => {
return ({
...state,
superBlocks: payload.result.superBlocks
}),
});
},
[challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({
...state,
currentChallenge: dashedName

View File

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

View File

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

View File

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