Feat(Challenges): no js preview (#16149)

* 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
This commit is contained in:
Berkeley Martinez
2017-12-07 16:13:19 -08:00
committed by Quincy Larson
parent 9051faee79
commit 2e410330f1
40 changed files with 771 additions and 626 deletions

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import { Observable } from 'rx';
import {
combineActions,
@@ -7,18 +8,21 @@ import {
handleActions
} from 'berkeleys-redux-utils';
import { createSelector } from 'reselect';
import noop from 'lodash/noop';
import identity from 'lodash/identity';
import { entitiesSelector } from '../entities';
import fetchUserEpic from './fetch-user-epic.js';
import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
import fetchChallengesEpic from './fetch-challenges-epic.js';
import navSizeEpic from './nav-size-epic.js';
import { createFilesMetaCreator } from '../files';
import { updateThemeMetacreator, entitiesSelector } from '../entities';
import { types as challenges } from '../routes/Challenges/redux';
import { challengeToFiles } from '../routes/Challenges/utils';
import ns from '../ns.json';
import { themes, invertTheme } from '../../utils/themes.js';
export const epics = [
fetchUserEpic,
fetchChallengesEpic,
@@ -36,9 +40,7 @@ export const types = createTypes([
createAsyncTypes('fetchChallenge'),
createAsyncTypes('fetchChallenges'),
'fetchUser',
'addUser',
'updateThisUser',
createAsyncTypes('fetchUser'),
'showSignIn',
'handleError',
@@ -48,8 +50,7 @@ export const types = createTypes([
// night mode
'toggleNightMode',
'updateTheme',
'addThemeToBody'
'postThemeComplete'
], ns);
const throwIfUndefined = () => {
@@ -95,7 +96,10 @@ export const fetchChallenge = createAction(
export const fetchChallengeCompleted = createAction(
types.fetchChallenge.complete,
null,
identity
meta => ({
...meta,
..._.flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
})
);
export const fetchChallenges = createAction('' + types.fetchChallenges);
export const fetchChallengesCompleted = createAction(
@@ -110,16 +114,12 @@ export const updateTitle = createAction(types.updateTitle);
// fetchUser() => Action
// used in combination with fetch-user-epic
export const fetchUser = createAction(types.fetchUser);
// addUser(
// entities: { [userId]: User }
// ) => Action
export const addUser = createAction(
types.addUser,
noop,
entities => ({ entities })
export const fetchUserComplete = createAction(
types.fetchUser.complete,
({ result }) => result,
_.identity
);
export const updateThisUser = createAction(types.updateThisUser);
export const showSignIn = createAction(types.showSignIn);
// used when server needs client to redirect
@@ -145,21 +145,20 @@ export const doActionOnError = actionCreator => error => Observable.of(
export const toggleNightMode = createAction(
types.toggleNightMode,
// we use this function to avoid hanging onto the eventObject
// so that react can recycle it
() => null
null,
(username, theme) => updateThemeMetacreator(username, invertTheme(theme))
);
export const postThemeComplete = createAction(
types.postThemeComplete,
null,
updateThemeMetacreator
);
// updateTheme(theme: /night|default/) => Action
export const updateTheme = createAction(types.updateTheme);
// addThemeToBody(theme: /night|default/) => Action
export const addThemeToBody = createAction(types.addThemeToBody);
const initialState = {
const defaultState = {
title: 'Learn To Code | freeCodeCamp',
isSignInAttempted: false,
user: '',
csrfToken: '',
theme: 'default',
// eventually this should be only in the user object
currentChallenge: '',
superBlocks: []
@@ -167,28 +166,37 @@ const initialState = {
export const getNS = state => state[ns];
export const csrfSelector = state => getNS(state).csrfToken;
export const themeSelector = state => getNS(state).theme;
export const titleSelector = state => getNS(state).title;
export const currentChallengeSelector = state => getNS(state).currentChallenge;
export const superBlocksSelector = state => getNS(state).superBlocks;
export const signInLoadingSelector = state => !getNS(state).isSignInAttempted;
export const usernameSelector = state => getNS(state).user || '';
export const userSelector = createSelector(
state => getNS(state).user,
state => entitiesSelector(state).user,
(username, userMap) => userMap[username] || {}
);
export const themeSelector = _.flow(
userSelector,
user => user.theme || themes.default
);
export const isSignedInSelector = state => !!userSelector(state).username;
export const challengeSelector = createSelector(
currentChallengeSelector,
state => entitiesSelector(state).challenge,
(challengeName, challengeMap = {}) => {
return challengeMap[challengeName] || {};
}
);
export const challengeSelector = state => {
const challengeName = currentChallengeSelector(state);
const challengeMap = entitiesSelector(state).challenge || {};
return challengeMap[challengeName] || {};
};
export const previousSolutionSelector = state => {
const { id } = challengeSelector(state);
const { challengeMap = {} } = userSelector(state);
return challengeMap[id];
};
export const firstChallengeSelector = createSelector(
entitiesSelector,
@@ -231,7 +239,7 @@ export default handleActions(
title: payload + ' | freeCodeCamp'
}),
[types.updateThisUser]: (state, { payload: user }) => ({
[types.fetchUser.complete]: (state, { payload: user }) => ({
...state,
user
}),
@@ -246,11 +254,9 @@ export default handleActions(
...state,
currentChallenge: dashedName
}),
[types.updateTheme]: (state, { payload = 'default' }) => ({
...state,
theme: payload
}),
[combineActions(types.showSignIn, types.updateThisUser)]: state => ({
[
combineActions(types.showSignIn, types.fetchUser.complete)
]: state => ({
...state,
isSignInAttempted: true
}),
@@ -264,6 +270,6 @@ export default handleActions(
delayedRedirect: payload
})
}),
initialState,
defaultState,
ns
);