* fix(files): Decouple files from challenges * feat(server/react): Remove action logger use redux remote devtools instead! * feat(Challenges): Disable js on edit, enable on execute * feat(Challenge/Preview): Show message when js is disabled * refactor(frameEpic): Reduce code by using lodash * feat(frameEpic): Disable js in preview by state * feat(frameEpic): Colocate epic in Challenges/redux * refactor(ExecuteChallengeEpic): CoLocated with Challenges * refactor(executeChallengesEpic): Separate tests from main logic * feat(Challenge/Preview): Update main on edit * feat(frameEpuc): Replace frame on edit/execute This allows for sandbox to work properly * fix(Challenges/Utils): Require utisl * revert(frameEpic): Hoist function to mount code in frame * fix(frameEpic): Ensure new frame is given classname * feat(executeChallenge): Update main on code unlocked * fix(frameEpic): Filter out empty test message * fix(Challenge/Preview): Remove unnessary quote in classname * feat(codeStorageEpic): Separate localstorage from solutions loading * fix(fetchUser): Merge user actions into one prefer many effects from one action over one action to one effect * fix(themes): Centralize theme utils and defs * fix(entities.user): Fix user reducer namespacing * feat(frame): Refactor frameEpic to util * feat(Challenges.redux): Should not attempt to update main from storage * fix(loadPreviousChallengeEpic): Refactor for RFR * fix(Challenges.Modern): Show preview plane
130 lines
4.2 KiB
JavaScript
130 lines
4.2 KiB
JavaScript
import _ from 'lodash';
|
|
import debug from 'debug';
|
|
import { Observable } from 'rx';
|
|
import { combineEpics, ofType } from 'redux-epic';
|
|
|
|
import {
|
|
types,
|
|
|
|
challengeUpdated,
|
|
onRouteChallenges,
|
|
onRouteCurrentChallenge
|
|
} from './';
|
|
import { getNS as entitiesSelector } from '../../../entities';
|
|
import {
|
|
getNextChallenge,
|
|
getFirstChallengeOfNextBlock,
|
|
getFirstChallengeOfNextSuperBlock
|
|
} from '../utils';
|
|
import {
|
|
createErrorObservable,
|
|
currentChallengeSelector,
|
|
challengeSelector,
|
|
superBlocksSelector
|
|
} 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 }) {
|
|
return actions::ofType(types.onRouteChallenges)
|
|
// prevent subsequent onRouteChallenges to cause UI to refresh
|
|
.distinctUntilChanged(({ payload: { dashedName }}) => dashedName)
|
|
.map(() => challengeSelector(getState()))
|
|
// if the challenge isn't loaded in the current state,
|
|
// this will be an empty object
|
|
// We wait instead for the fetchChallenge.complete to complete the UI state
|
|
.filter(({ dashedName }) => !!dashedName)
|
|
// send the challenge to update UI and trigger main iframe to update
|
|
// use unary to prevent index from being passed to func
|
|
.map(_.unary(challengeUpdated));
|
|
}
|
|
|
|
// used to reset users code on request
|
|
export function resetChallengeEpic(actions, { getState }) {
|
|
return actions::ofType(types.clickOnReset)
|
|
.map(_.flow(getState, challengeSelector, challengeUpdated));
|
|
}
|
|
|
|
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 }
|
|
);
|
|
}
|
|
// 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);
|
|
}
|
|
});
|
|
}
|
|
|
|
export default combineEpics(
|
|
challengeUpdatedEpic,
|
|
nextChallengeEpic,
|
|
resetChallengeEpic
|
|
);
|