freeCodeCamp/common/app/redux/index.js

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
);