* 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
		
			
				
	
	
		
			276 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import _ from 'lodash';
 | 
						|
import { Observable } from 'rx';
 | 
						|
import {
 | 
						|
  combineActions,
 | 
						|
  createAction,
 | 
						|
  createAsyncTypes,
 | 
						|
  createTypes,
 | 
						|
  handleActions
 | 
						|
} from 'berkeleys-redux-utils';
 | 
						|
import { createSelector } from 'reselect';
 | 
						|
 | 
						|
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,
 | 
						|
  updateMyCurrentChallengeEpic,
 | 
						|
  navSizeEpic
 | 
						|
];
 | 
						|
 | 
						|
export const types = createTypes([
 | 
						|
  'onRouteHome',
 | 
						|
 | 
						|
  'appMounted',
 | 
						|
  'analytics',
 | 
						|
  'updateTitle',
 | 
						|
 | 
						|
  createAsyncTypes('fetchChallenge'),
 | 
						|
  createAsyncTypes('fetchChallenges'),
 | 
						|
 | 
						|
  createAsyncTypes('fetchUser'),
 | 
						|
  'showSignIn',
 | 
						|
 | 
						|
  'handleError',
 | 
						|
  // used to hit the server
 | 
						|
  'hardGoTo',
 | 
						|
  'delayedRedirect',
 | 
						|
 | 
						|
  // night mode
 | 
						|
  'toggleNightMode',
 | 
						|
  'postThemeComplete'
 | 
						|
], ns);
 | 
						|
 | 
						|
const throwIfUndefined = () => {
 | 
						|
  throw new TypeError('Argument must not be of  type `undefined`');
 | 
						|
};
 | 
						|
 | 
						|
// createEventMetaCreator({
 | 
						|
//   category: String,
 | 
						|
//   action: String,
 | 
						|
//   label?: String,
 | 
						|
//   value?: Number
 | 
						|
// }) => () => Object
 | 
						|
export const createEventMetaCreator = ({
 | 
						|
  // categories are features or namespaces of the app (capitalized):
 | 
						|
  //   Map, Nav, Challenges, and so on
 | 
						|
  category = throwIfUndefined,
 | 
						|
  // can be a one word the event
 | 
						|
  // click, play, toggle.
 | 
						|
  // This is not a hard and fast rule
 | 
						|
  action = throwIfUndefined,
 | 
						|
  // any additional information
 | 
						|
  // when in doubt use redux action type
 | 
						|
  // or a short sentence describing the action
 | 
						|
  label,
 | 
						|
  // used to tack some specific value for a GA event
 | 
						|
  value
 | 
						|
} = throwIfUndefined) => () => ({
 | 
						|
  analytics: {
 | 
						|
    type: 'event',
 | 
						|
    category,
 | 
						|
    action,
 | 
						|
    label,
 | 
						|
    value
 | 
						|
  }
 | 
						|
});
 | 
						|
 | 
						|
export const onRouteHome = createAction(types.onRouteHome);
 | 
						|
export const appMounted = createAction(types.appMounted);
 | 
						|
export const fetchChallenge = createAction(
 | 
						|
  '' + types.fetchChallenge,
 | 
						|
  (dashedName, block) => ({ dashedName, block })
 | 
						|
);
 | 
						|
export const fetchChallengeCompleted = createAction(
 | 
						|
  types.fetchChallenge.complete,
 | 
						|
  null,
 | 
						|
  meta => ({
 | 
						|
    ...meta,
 | 
						|
    ..._.flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
 | 
						|
  })
 | 
						|
);
 | 
						|
export const fetchChallenges = createAction('' + types.fetchChallenges);
 | 
						|
export const fetchChallengesCompleted = createAction(
 | 
						|
  types.fetchChallenges.complete,
 | 
						|
  (entities, result) => ({ entities, result }),
 | 
						|
  entities => ({ entities })
 | 
						|
);
 | 
						|
 | 
						|
// updateTitle(title: String) => Action
 | 
						|
export const updateTitle = createAction(types.updateTitle);
 | 
						|
 | 
						|
// fetchUser() => Action
 | 
						|
// used in combination with fetch-user-epic
 | 
						|
export const fetchUser = createAction(types.fetchUser);
 | 
						|
export const fetchUserComplete = createAction(
 | 
						|
  types.fetchUser.complete,
 | 
						|
  ({ result }) => result,
 | 
						|
  _.identity
 | 
						|
);
 | 
						|
 | 
						|
export const showSignIn = createAction(types.showSignIn);
 | 
						|
 | 
						|
// used when server needs client to redirect
 | 
						|
export const delayedRedirect = createAction(types.delayedRedirect);
 | 
						|
 | 
						|
// hardGoTo(path: String) => Action
 | 
						|
export const hardGoTo = createAction(types.hardGoTo);
 | 
						|
 | 
						|
export const createErrorObservable = error => Observable.just({
 | 
						|
  type: types.handleError,
 | 
						|
  error
 | 
						|
});
 | 
						|
// doActionOnError(
 | 
						|
//   actionCreator: (() => Action|Null)
 | 
						|
// ) => (error: Error) => Observable[Action]
 | 
						|
export const doActionOnError = actionCreator => error => Observable.of(
 | 
						|
  {
 | 
						|
    type: types.handleError,
 | 
						|
    error
 | 
						|
  },
 | 
						|
  actionCreator()
 | 
						|
);
 | 
						|
 | 
						|
export const toggleNightMode = createAction(
 | 
						|
  types.toggleNightMode,
 | 
						|
  null,
 | 
						|
  (username, theme) => updateThemeMetacreator(username, invertTheme(theme))
 | 
						|
);
 | 
						|
export const postThemeComplete = createAction(
 | 
						|
  types.postThemeComplete,
 | 
						|
  null,
 | 
						|
  updateThemeMetacreator
 | 
						|
);
 | 
						|
 | 
						|
const defaultState = {
 | 
						|
  title: 'Learn To Code | freeCodeCamp',
 | 
						|
  isSignInAttempted: false,
 | 
						|
  user: '',
 | 
						|
  csrfToken: '',
 | 
						|
  // eventually this should be only in the user object
 | 
						|
  currentChallenge: '',
 | 
						|
  superBlocks: []
 | 
						|
};
 | 
						|
 | 
						|
export const getNS = state => state[ns];
 | 
						|
export const csrfSelector = state => getNS(state).csrfToken;
 | 
						|
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 = 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,
 | 
						|
  superBlocksSelector,
 | 
						|
  (
 | 
						|
    {
 | 
						|
      challengeMap,
 | 
						|
      blockMap,
 | 
						|
      superBlockMap
 | 
						|
    },
 | 
						|
    superBlocks
 | 
						|
  ) => {
 | 
						|
    if (
 | 
						|
      !challengeMap ||
 | 
						|
      !blockMap ||
 | 
						|
      !superBlockMap ||
 | 
						|
      !superBlocks
 | 
						|
    ) {
 | 
						|
      return {};
 | 
						|
    }
 | 
						|
    try {
 | 
						|
      return challengeMap[
 | 
						|
        blockMap[
 | 
						|
          superBlockMap[
 | 
						|
            superBlocks[0]
 | 
						|
          ].blocks[0]
 | 
						|
        ].challenges[0]
 | 
						|
      ];
 | 
						|
    } catch (err) {
 | 
						|
      console.error(err);
 | 
						|
      return {};
 | 
						|
    }
 | 
						|
  }
 | 
						|
);
 | 
						|
 | 
						|
export default handleActions(
 | 
						|
  () => ({
 | 
						|
    [types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
 | 
						|
      ...state,
 | 
						|
      title: payload + ' | freeCodeCamp'
 | 
						|
    }),
 | 
						|
 | 
						|
    [types.fetchUser.complete]: (state, { payload: user }) => ({
 | 
						|
      ...state,
 | 
						|
      user
 | 
						|
    }),
 | 
						|
    [combineActions(
 | 
						|
      types.fetchChallenge.complete,
 | 
						|
      types.fetchChallenges.complete
 | 
						|
    )]: (state, { payload }) => ({
 | 
						|
      ...state,
 | 
						|
      superBlocks: payload.result.superBlocks
 | 
						|
    }),
 | 
						|
    [challenges.onRouteChallenges]: (state, { payload: { dashedName } }) => ({
 | 
						|
      ...state,
 | 
						|
      currentChallenge: dashedName
 | 
						|
    }),
 | 
						|
    [
 | 
						|
      combineActions(types.showSignIn, types.fetchUser.complete)
 | 
						|
    ]: state => ({
 | 
						|
      ...state,
 | 
						|
      isSignInAttempted: true
 | 
						|
    }),
 | 
						|
 | 
						|
    [types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
 | 
						|
      ...state,
 | 
						|
      points
 | 
						|
    }),
 | 
						|
    [types.delayedRedirect]: (state, { payload }) => ({
 | 
						|
      ...state,
 | 
						|
      delayedRedirect: payload
 | 
						|
    })
 | 
						|
  }),
 | 
						|
  defaultState,
 | 
						|
  ns
 | 
						|
);
 |