diff --git a/common/app/Map/Block.jsx b/common/app/Map/Block.jsx index 5f72d6f1f8..1387faa0bd 100644 --- a/common/app/Map/Block.jsx +++ b/common/app/Map/Block.jsx @@ -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 ( fetchNewBlock(dashedName) } onSelect={ this.handleSelect } > { isOpen && } @@ -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); diff --git a/common/app/Map/Map.jsx b/common/app/Map/Map.jsx index 056bab9926..783543c614 100644 --- a/common/app/Map/Map.jsx +++ b/common/app/Map/Map.jsx @@ -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
No Super Blocks
; + return ( +
+ +
+ ); } return superBlocks.map(dashedName => (
- { this.renderSuperBlocks(superBlocks) } + { this.renderSuperBlocks() }
diff --git a/common/app/Map/Super-Block.jsx b/common/app/Map/Super-Block.jsx index 6d7038d2a3..c5ce006263 100644 --- a/common/app/Map/Super-Block.jsx +++ b/common/app/Map/Super-Block.jsx @@ -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 ( -
- { message } -
- ); - } - renderHeader(isOpen, title, isCompleted) { return (
@@ -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) } ); diff --git a/common/app/Map/redux/fetch-map-ui-epic.js b/common/app/Map/redux/fetch-map-ui-epic.js new file mode 100644 index 0000000000..9b129f8560 --- /dev/null +++ b/common/app/Map/redux/fetch-map-ui-epic.js @@ -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); + }); + } diff --git a/common/app/Map/redux/index.js b/common/app/Map/redux/index.js index 62b9d15b95..49d48752ee 100644 --- a/common/app/Map/redux/index.js +++ b/common/app/Map/redux/index.js @@ -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) }; } diff --git a/common/app/entities/index.js b/common/app/entities/index.js index d8dae89a97..8a85e99a11 100644 --- a/common/app/entities/index.js +++ b/common/app/entities/index.js @@ -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 } }) => ({ diff --git a/common/app/redux/fetch-challenges-epic.js b/common/app/redux/fetch-challenges-epic.js index 6873507cc6..b0d0383ee9 100644 --- a/common/app/redux/fetch-challenges-epic.js +++ b/common/app/redux/fetch-challenges-epic.js @@ -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 +); diff --git a/common/app/redux/index.js b/common/app/redux/index.js index b0a634f0b7..d878ffc01f 100644 --- a/common/app/redux/index.js +++ b/common/app/redux/index.js @@ -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,15 +90,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 +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 diff --git a/common/app/redux/utils.js b/common/app/redux/utils.js index 968ab97e60..b7e4aefaa8 100644 --- a/common/app/redux/utils.js +++ b/common/app/redux/utils.js @@ -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) diff --git a/common/app/routes/Challenges/Completion-Modal.jsx b/common/app/routes/Challenges/Completion-Modal.jsx index 49c986f0db..6ef24ea35d 100644 --- a/common/app/routes/Challenges/Completion-Modal.jsx +++ b/common/app/routes/Challenges/Completion-Modal.jsx @@ -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, diff --git a/common/app/routes/Challenges/Show.jsx b/common/app/routes/Challenges/Show.jsx index b203a81705..f8a4ac62ab 100644 --- a/common/app/routes/Challenges/Show.jsx +++ b/common/app/routes/Challenges/Show.jsx @@ -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, diff --git a/common/app/routes/Challenges/redux/challenge-epic.js b/common/app/routes/Challenges/redux/challenge-epic.js index f9fb948403..a0a9056c4d 100644 --- a/common/app/routes/Challenges/redux/challenge-epic.js +++ b/common/app/routes/Challenges/redux/challenge-epic.js @@ -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,73 +43,33 @@ 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. - if (!nextChallenge) { - // isNewBlock = true; - nextChallenge = getFirstChallengeOfNextBlock( - challenge, - entities, - { isDev } + const state = getState(); + const lang = langSelector(state); + const { nextChallenge } = nextChallengeSelector(state); + if (!nextChallenge) { + 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({ - 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.' }) + ); }); } diff --git a/common/app/routes/Challenges/redux/index.js b/common/app/routes/Challenges/redux/index.js index 090de13004..7d92931a69 100644 --- a/common/app/routes/Challenges/redux/index.js +++ b/common/app/routes/Challenges/redux/index.js @@ -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); diff --git a/common/app/routes/Settings/components/Internet-Settings.jsx b/common/app/routes/Settings/components/Internet-Settings.jsx index 72e5ab6574..ee96f1983e 100644 --- a/common/app/routes/Settings/components/Internet-Settings.jsx +++ b/common/app/routes/Settings/components/Internet-Settings.jsx @@ -54,7 +54,6 @@ class InternetSettings extends PureComponent { } handleSubmit(values) { - console.log(values); this.props.updateUserBackend(values); } diff --git a/server/boot/a-services.js b/server/boot/a-services.js index f9bdeb60c1..4bbe43a936 100644 --- a/server/boot/a-services.js +++ b/server/boot/a-services.js @@ -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()); } diff --git a/server/services/challenge.js b/server/services/challenge.js new file mode 100644 index 0000000000..53b92fbdb2 --- /dev/null +++ b/server/services/challenge.js @@ -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 + ); + } + }; +} diff --git a/server/services/map.js b/server/services/map.js deleted file mode 100644 index 7c8b71eee2..0000000000 --- a/server/services/map.js +++ /dev/null @@ -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); } - ); - } - }; -} diff --git a/server/services/mapUi.js b/server/services/mapUi.js new file mode 100644 index 0000000000..b7dca26595 --- /dev/null +++ b/server/services/mapUi.js @@ -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); } + ); + } + }; +}