357 lines
9.0 KiB
JavaScript
357 lines
9.0 KiB
JavaScript
import { flow, identity } from 'lodash';
|
|
import { Observable } from 'rx';
|
|
import {
|
|
combineActions,
|
|
createAction,
|
|
createAsyncTypes,
|
|
createTypes,
|
|
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';
|
|
import fetchChallengesEpic from './fetch-challenges-epic.js';
|
|
import nightModeEpic from './night-mode-epic.js';
|
|
|
|
import { createFilesMetaCreator } from '../files';
|
|
import { updateThemeMetacreator, entitiesSelector } from '../entities';
|
|
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,
|
|
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,
|
|
nightModeEpic,
|
|
updateMyCurrentChallengeEpic
|
|
];
|
|
|
|
export const types = createTypes([
|
|
'onRouteHome',
|
|
|
|
'appMounted',
|
|
'analytics',
|
|
'updateTitle',
|
|
|
|
createAsyncTypes('fetchChallenge'),
|
|
createAsyncTypes('fetchChallenges'),
|
|
createAsyncTypes('fetchNewBlock'),
|
|
'updateChallenges',
|
|
createAsyncTypes('fetchOtherUser'),
|
|
createAsyncTypes('fetchUser'),
|
|
'showSignIn',
|
|
|
|
'handleError',
|
|
// used to hit the server
|
|
'hardGoTo',
|
|
'delayedRedirect',
|
|
|
|
// night mode
|
|
'toggleNightMode',
|
|
createAsyncTypes('postTheme')
|
|
], ns);
|
|
|
|
const throwIfUndefined = () => {
|
|
throw new TypeError('Argument must not be of type `undefined`');
|
|
};
|
|
|
|
// createEventMetaCreator({
|
|
// category: String,
|
|
// action: String,
|
|
// label?: String,
|
|
// value?: Number
|
|
// }) => () => Object
|
|
export function 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) {
|
|
return () => ({
|
|
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
|
|
);
|
|
|
|
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
|
|
export const updateTitle = createAction(types.updateTitle);
|
|
|
|
// fetchOtherUser() => Action
|
|
// used in combination with fetch-user-epic
|
|
// to fetch another users profile
|
|
export const fetchOtherUser = createAction(types.fetchOtherUser.start);
|
|
export const fetchOtherUserComplete = createAction(
|
|
types.fetchOtherUser.complete,
|
|
({ result }) => result,
|
|
identity
|
|
);
|
|
|
|
// 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
|
|
});
|
|
// use sparingly
|
|
// 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.postTheme.complete,
|
|
null,
|
|
utils.createFlashMetaAction
|
|
);
|
|
|
|
export const postThemeError = createAction(
|
|
types.postTheme.error,
|
|
null,
|
|
(username, theme, err) => ({
|
|
...updateThemeMetacreator(username, invertTheme(theme)),
|
|
...utils.createFlashMetaAction(err)
|
|
})
|
|
);
|
|
|
|
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 userByNameSelector = state => {
|
|
const username = paramsSelector(state).username;
|
|
const userMap = entitiesSelector(state).user;
|
|
return 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,
|
|
(
|
|
{
|
|
challenge: challengeMap,
|
|
block: blockMap,
|
|
superBlock: 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 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' }) => ({
|
|
...state,
|
|
title: payload + ' | freeCodeCamp'
|
|
}),
|
|
|
|
[types.fetchUser.complete]: (state, { payload: user }) => ({
|
|
...state,
|
|
user
|
|
}),
|
|
[combineActions(
|
|
types.fetchChallenge.complete,
|
|
map.fetchMapUi.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
|
|
);
|