280 lines
7.2 KiB
JavaScript
280 lines
7.2 KiB
JavaScript
import { Observable } from 'rx';
|
|
import { createTypes, createAsyncTypes } from 'redux-create-types';
|
|
import { combineActions, createAction, handleActions } from 'redux-actions';
|
|
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 ns from '../ns.json';
|
|
|
|
export const epics = [
|
|
fetchUserEpic,
|
|
fetchChallengesEpic,
|
|
updateMyCurrentChallengeEpic,
|
|
navSizeEpic
|
|
];
|
|
|
|
export const types = createTypes([
|
|
'appMounted',
|
|
'analytics',
|
|
'updateTitle',
|
|
'updateAppLang',
|
|
|
|
createAsyncTypes('fetchChallenge'),
|
|
createAsyncTypes('fetchChallenges'),
|
|
'updateCurrentChallenge',
|
|
|
|
'fetchUser',
|
|
'addUser',
|
|
'updateThisUser',
|
|
'showSignIn',
|
|
|
|
'handleError',
|
|
// used to hit the server
|
|
'hardGoTo',
|
|
'delayedRedirect',
|
|
|
|
// night mode
|
|
'toggleNightMode',
|
|
'updateTheme',
|
|
'addThemeToBody'
|
|
], 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 appMounted = createAction(types.appMounted);
|
|
export const fetchChallenge = createAction(
|
|
'' + types.fetchChallenge,
|
|
(dashedName, block) => ({ dashedName, block })
|
|
);
|
|
export const fetchChallengeCompleted = createAction(
|
|
types.fetchChallenge.complete,
|
|
null,
|
|
identity
|
|
);
|
|
export const fetchChallenges = createAction('' + types.fetchChallenges);
|
|
export const fetchChallengesCompleted = createAction(
|
|
types.fetchChallenges.complete,
|
|
(entities, result) => ({ entities, result }),
|
|
entities => ({ entities })
|
|
);
|
|
export const updateCurrentChallenge = createAction(
|
|
types.updateCurrentChallenge
|
|
);
|
|
|
|
// 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);
|
|
|
|
// addUser(
|
|
// entities: { [userId]: User }
|
|
// ) => Action
|
|
export const addUser = createAction(
|
|
types.addUser,
|
|
noop,
|
|
entities => ({ entities })
|
|
);
|
|
export const updateThisUser = createAction(types.updateThisUser);
|
|
export const showSignIn = createAction(types.showSignIn);
|
|
|
|
export const updateAppLang = createAction(types.updateAppLang);
|
|
|
|
// 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,
|
|
// we use this function to avoid hanging onto the eventObject
|
|
// so that react can recycle it
|
|
() => null
|
|
);
|
|
// updateTheme(theme: /night|default/) => Action
|
|
export const updateTheme = createAction(types.updateTheme);
|
|
// addThemeToBody(theme: /night|default/) => Action
|
|
export const addThemeToBody = createAction(types.addThemeToBody);
|
|
|
|
const initialState = {
|
|
title: 'Learn To Code | freeCodeCamp',
|
|
isSignInAttempted: false,
|
|
user: '',
|
|
lang: '',
|
|
csrfToken: '',
|
|
theme: 'default',
|
|
// eventually this should be only in the user object
|
|
currentChallenge: '',
|
|
superBlocks: []
|
|
};
|
|
|
|
export const getNS = state => state[ns];
|
|
export const langSelector = state => getNS(state).lang;
|
|
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 userSelector = createSelector(
|
|
state => getNS(state).user,
|
|
state => entitiesSelector(state).user,
|
|
(username, userMap) => userMap[username] || {}
|
|
);
|
|
|
|
export const challengeSelector = createSelector(
|
|
currentChallengeSelector,
|
|
state => entitiesSelector(state).challenge,
|
|
(challengeName, challengeMap = {}) => {
|
|
return challengeMap[challengeName] || {};
|
|
}
|
|
);
|
|
|
|
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 function createReducer() {
|
|
const reducer = handleActions(
|
|
{
|
|
[types.updateTitle]: (state, { payload = 'Learn To Code' }) => ({
|
|
...state,
|
|
title: payload + ' | freeCodeCamp'
|
|
}),
|
|
|
|
[types.updateThisUser]: (state, { payload: user }) => ({
|
|
...state,
|
|
user
|
|
}),
|
|
[types.fetchChallenge.complete]: (state, { payload }) => ({
|
|
...state,
|
|
currentChallenge: payload.currentChallenge
|
|
}),
|
|
[combineActions(
|
|
types.fetchChallenge.complete,
|
|
types.fetchChallenges.complete
|
|
)]: (state, { payload }) => ({
|
|
...state,
|
|
superBlocks: payload.result.superBlocks
|
|
}),
|
|
[types.updateCurrentChallenge]: (state, { payload = '' }) => ({
|
|
...state,
|
|
currentChallenge: payload
|
|
}),
|
|
[types.updateAppLang]: (state, { payload = 'en' }) =>({
|
|
...state,
|
|
lang: payload
|
|
}),
|
|
[types.updateTheme]: (state, { payload = 'default' }) => ({
|
|
...state,
|
|
theme: payload
|
|
}),
|
|
[combineActions(types.showSignIn, types.updateThisUser)]: state => ({
|
|
...state,
|
|
isSignInAttempted: true
|
|
}),
|
|
|
|
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
|
|
...state,
|
|
points
|
|
}),
|
|
[types.delayedRedirect]: (state, { payload }) => ({
|
|
...state,
|
|
delayedRedirect: payload
|
|
})
|
|
},
|
|
initialState
|
|
);
|
|
|
|
reducer.toString = () => ns;
|
|
return reducer;
|
|
}
|