From 329438bdf4cf5991c0677743266309263077338d Mon Sep 17 00:00:00 2001 From: Stuart Taylor Date: Sat, 24 Feb 2018 08:44:12 +0000 Subject: [PATCH] fix(nextChallenge): Remove some duplication --- common/app/Map/Map.jsx | 2 +- common/app/redux/fetch-challenges-epic.js | 54 +++------ common/app/redux/index.js | 50 ++++++++- .../routes/Challenges/redux/challenge-epic.js | 103 +++++------------- server/services/challenge.js | 30 ++--- 5 files changed, 102 insertions(+), 137 deletions(-) diff --git a/common/app/Map/Map.jsx b/common/app/Map/Map.jsx index b942992e5e..783543c614 100644 --- a/common/app/Map/Map.jsx +++ b/common/app/Map/Map.jsx @@ -26,7 +26,7 @@ export class ShowMap extends PureComponent { const { superBlocks } = this.props; if (!Array.isArray(superBlocks) || !superBlocks.length) { return ( -
+
); diff --git a/common/app/redux/fetch-challenges-epic.js b/common/app/redux/fetch-challenges-epic.js index faecb7711a..b0d0383ee9 100644 --- a/common/app/redux/fetch-challenges-epic.js +++ b/common/app/redux/fetch-challenges-epic.js @@ -12,22 +12,15 @@ import { fetchChallengesCompleted, fetchNewBlock, challengeSelector, - superBlocksSelector, - currentChallengeSelector + nextChallengeSelector } from './'; import { isChallengeLoaded, - fullBlocksSelector, - entitiesSelector + fullBlocksSelector } from '../entities'; import { shapeChallenges } from './utils'; import { types as challenge } from '../routes/Challenges/redux'; -import { - getFirstChallengeOfNextBlock, - getFirstChallengeOfNextSuperBlock, - getNextChallenge -} from '../routes/Challenges/utils'; import { langSelector } from '../Router/redux'; const isDev = debug.enabled('fcc:*'); @@ -81,7 +74,7 @@ export function fetchChallengesForBlockEpic( if (fetchAnotherBlock) { const fullBlocks = fullBlocksSelector(state); if (fullBlocks.includes(payload)) { - return Observable.of({ type: 'NULL'}); + return Observable.of(null); } blockName = payload; } @@ -95,47 +88,28 @@ export function fetchChallengesForBlockEpic( .map(fetchChallengesCompleted) .startWith({ type: types.fetchChallenges.start }) .catch(createErrorObservable); - }); + }) + .filter(Boolean); } function fetchChallengesForNextBlockEpic(action$, { getState }) { return action$::ofType(challenge.checkForNextBlock) .map(() => { - let nextChallenge = {}; - let isNewBlock = false; - let isNewSuperBlock = false; - const state = getState(); - 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 } - ); - } + const { + nextChallenge, + isNewBlock, + isNewSuperBlock + } = nextChallengeSelector(getState()); const isNewBlockRequired = ( (isNewBlock || isNewSuperBlock) && + nextChallenge && !nextChallenge.description ); return isNewBlockRequired ? fetchNewBlock(nextChallenge.block) : - { type: 'NULL' }; - }); + null; + }) + .filter(Boolean); } export default combineEpics( diff --git a/common/app/redux/index.js b/common/app/redux/index.js index 553c18b75b..d878ffc01f 100644 --- a/common/app/redux/index.js +++ b/common/app/redux/index.js @@ -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'; @@ -20,12 +21,19 @@ 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 { + 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, @@ -271,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' }) => ({ @@ -285,12 +327,10 @@ export default handleActions( [combineActions( types.fetchChallenge.complete, map.fetchMapUi.complete - )]: (state, { payload }) => { - return ({ + )]: (state, { payload }) => ({ ...state, superBlocks: payload.result.superBlocks - }); - }, + }), [challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({ ...state, currentChallenge: dashedName 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/server/services/challenge.js b/server/services/challenge.js index e7d56488c2..53b92fbdb2 100644 --- a/server/services/challenge.js +++ b/server/services/challenge.js @@ -28,22 +28,22 @@ export default function getChallengesForBlock(app) { } }) => { 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 } - }); + 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),