* feat(seed): Add modern challenge * chore(react): Use prop-types package * feat: Initial refactor to redux-first-router BREAKING CHANGE: Everything is different! * feat: First rendering * feat(routes): Challenges view render but failing * fix(Challenges): Remove contain HOC * fix(RFR): Add params selector * fix(RFR): :en should be :lang * fix: Update berks utils for redux * fix(Map): Challenge link to arg * fix(Map): Add trailing slash to map page * fix(RFR): Use FCC Link Use fcc Link to get around issue of lang being undefined * fix(Router): Link to is required * fix(app): Rely on RFR state for app lang * chore(RFR): Remove unused RFR Link * fix(RFR): Hydrate initial challenge using RFR and RO * fix: Casing issue * fix(RFR): Undefined links * fix(RFR): Use onRoute<name> convention for route types * feat(server/react): Add helpful redux logging/throwing * fix(server/react): Strip out nonjson from state This prevents thunks in routesMap from breaking serialization * fix(RFR/Link): Should accept any renderable * fix(RFR): Get redirects working * fix(RFR): Redirects and not found's * fix(Map): Move challenge onClick handler * fix(Map): Allow Router.link to handle clicks after onClick * fix(routes): Remove react-router-redux * feat(Router): Add lang to all route actions by default * fix(entities): Only fetch challenge if not already loaded * fix(Files): Move files to own feature * chore(Challenges): Remove vestigial hints logic * fix(RFR): Update challenges on route challenges * fix(code-storage): Should use events instead of commands * fix(Map): ClickOnMap should not hold on to event * chore(lint): Use eslint-config-freecodecamp Closes #15938 * feat(Panes): Update panes on route instead of render * fix(Panes): Store panesmap and update on fetchchallenges * fix(Panes): Normalize panesmaps * fix(Panes): Remove filter from createpanemap * fix(Panes): Middleware on location meta object * feat(Panes): Filter preview on nonhtml challenges * build(babel): Add lodash babel plugin * chore(lint): Lint js files * fix(server/user-stats): Remove use of lodash chain this interferes with babel-plugin-lodash * feat(dev): Add remote redux devtools for ssr * fix(Panes): Dispatch mount action this is needed to trigger window/divider epics * fix(Panes): Getpane to use new panesmap format * fix(Panes): Always update panes after state this lets the panes logic be affected by changes in state
312 lines
9.0 KiB
JavaScript
312 lines
9.0 KiB
JavaScript
import { isLocationAction } from 'redux-first-router';
|
|
import _ from 'lodash';
|
|
import {
|
|
composeReducers,
|
|
createAction,
|
|
createTypes,
|
|
handleActions
|
|
} from 'berkeleys-redux-utils';
|
|
|
|
import ns from '../ns.json';
|
|
|
|
import windowEpic from './window-epic.js';
|
|
import dividerEpic from './divider-epic.js';
|
|
import { challengeMetaSelector } from '../../routes/Challenges/redux';
|
|
import { types as app } from '../../redux';
|
|
|
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
export const epics = [
|
|
windowEpic,
|
|
dividerEpic
|
|
];
|
|
|
|
export const types = createTypes([
|
|
'panesUpdatedThroughFetch',
|
|
'panesMounted',
|
|
'panesUpdated',
|
|
'panesWillMount',
|
|
'panesWillUnmount',
|
|
'updateSize',
|
|
|
|
'dividerClicked',
|
|
'dividerMoved',
|
|
'mouseReleased',
|
|
'windowResized',
|
|
|
|
// commands
|
|
'updateNavHeight',
|
|
'hidePane'
|
|
], ns);
|
|
|
|
export const panesUpdatedThroughFetch = createAction(
|
|
types.panesUpdatedThroughFetch,
|
|
null,
|
|
panesView => ({ panesView })
|
|
);
|
|
export const panesMounted = createAction(types.panesMounted);
|
|
export const panesUpdated = createAction(types.panesUpdated);
|
|
export const panesWillMount = createAction(types.panesWillMount);
|
|
export const panesWillUnmount = createAction(types.panesWillUnmount);
|
|
|
|
export const dividerClicked = createAction(types.dividerClicked);
|
|
export const dividerMoved = createAction(types.dividerMoved);
|
|
export const mouseReleased = createAction(types.mouseReleased);
|
|
export const windowResized = createAction(types.windowResized);
|
|
|
|
// commands
|
|
export const updateNavHeight = createAction(types.updateNavHeight);
|
|
export const hidePane = createAction(types.hidePane);
|
|
|
|
const initialState = {
|
|
height: 600,
|
|
width: 800,
|
|
navHeight: 50,
|
|
panes: [],
|
|
panesByName: {},
|
|
pressedDivider: null,
|
|
nameToType: {}
|
|
};
|
|
export const getNS = state => state[ns];
|
|
export const heightSelector = state => {
|
|
const { navHeight, height } = getNS(state);
|
|
return height - navHeight;
|
|
};
|
|
|
|
export const panesSelector = state => getNS(state).panes;
|
|
export const panesByNameSelector = state => getNS(state).panesByName;
|
|
export const pressedDividerSelector =
|
|
state => getNS(state).pressedDivider;
|
|
export const widthSelector = state => getNS(state).width;
|
|
export const nameToTypeSelector = state => getNS(state).nameToType;
|
|
|
|
function isPanesAction({ type } = {}, typeToName) {
|
|
return !!typeToName[type];
|
|
}
|
|
|
|
function getDividerLeft(numOfPanes, index) {
|
|
let dividerLeft = null;
|
|
if (numOfPanes > 1 && numOfPanes !== index + 1) {
|
|
dividerLeft = (100 / numOfPanes) * (index + 1);
|
|
}
|
|
return dividerLeft;
|
|
}
|
|
|
|
function forEachConfig(config, cb) {
|
|
return _.forEach(config, (val, key) => {
|
|
// val is a sub config
|
|
if (_.isObject(val) && !val.name) {
|
|
return forEachConfig(val, cb);
|
|
}
|
|
return cb(config, key);
|
|
});
|
|
}
|
|
|
|
function reduceConfig(config, cb, acc = {}) {
|
|
return _.reduce(config, (acc, val, key) => {
|
|
if (_.isObject(val) && !val.name) {
|
|
return reduceConfig(val, cb, acc);
|
|
}
|
|
return cb(acc, val, key);
|
|
}, acc);
|
|
}
|
|
|
|
const getPaneName = (panes, index) => (panes[index] || {}).name || '';
|
|
|
|
export const createPaneMap = (ns, getPanesMap) => {
|
|
const panesMap = _.reduce(getPanesMap(), (map, val, key) => {
|
|
let paneConfig = val;
|
|
if (typeof val === 'string') {
|
|
paneConfig = {
|
|
name: val
|
|
};
|
|
}
|
|
map[key] = paneConfig;
|
|
return map;
|
|
}, {});
|
|
return Object.defineProperty(panesMap, 'toString', { value: () => ns });
|
|
};
|
|
|
|
|
|
export default function createPanesAspects(config) {
|
|
if (isDev) {
|
|
forEachConfig(config, (typeToName, actionType) => {
|
|
if (actionType === 'undefined') {
|
|
throw new Error(
|
|
`action type for ${typeToName[actionType]} is undefined`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
const typeToName = reduceConfig(config, (acc, val, type) => {
|
|
const name = _.isObject(val) ? val.name : val;
|
|
acc[type] = name;
|
|
return acc;
|
|
});
|
|
|
|
function middleware({ getState }) {
|
|
const filterPanes = panesMap => _.reduce(panesMap, (panes, pane, type) => {
|
|
if (typeof pane.filter !== 'function' || pane.filter(getState())) {
|
|
panes[type] = pane;
|
|
}
|
|
return panes;
|
|
}, {});
|
|
// we cache the previous map so that we can attach it to the fetchChallenge
|
|
let previousMap;
|
|
// show panes on challenge route
|
|
// select panes map on viewType (this is state dependent)
|
|
// filter panes out on state
|
|
return next => action => {
|
|
let finalAction = action;
|
|
if (isPanesAction(action, typeToName)) {
|
|
finalAction = {
|
|
...action,
|
|
meta: {
|
|
...action.meta,
|
|
isPaneAction: true,
|
|
paneName: typeToName[action.type]
|
|
}
|
|
};
|
|
}
|
|
const result = next(finalAction);
|
|
if (isLocationAction(action)) {
|
|
// location matches a panes route
|
|
if (config[action.type]) {
|
|
const paneMap = previousMap = config[action.type];
|
|
const meta = challengeMetaSelector(getState());
|
|
const viewMap = paneMap[meta.viewType] || {};
|
|
next(panesUpdatedThroughFetch(filterPanes(viewMap)));
|
|
} else {
|
|
next(panesUpdatedThroughFetch({}));
|
|
}
|
|
}
|
|
if (action.type === app.fetchChallenge.complete) {
|
|
const meta = challengeMetaSelector(getState());
|
|
const viewMap = previousMap[meta.viewType] || {};
|
|
next(panesUpdatedThroughFetch(filterPanes(viewMap)));
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
const reducer = composeReducers(
|
|
ns,
|
|
handleActions(
|
|
() => ({
|
|
[types.dividerClicked]: (state, { payload: name }) => ({
|
|
...state,
|
|
pressedDivider: name
|
|
}),
|
|
[types.dividerMoved]: (state, { payload: clientX }) => {
|
|
const { width, pressedDivider: paneName } = state;
|
|
const dividerBuffer = (200 / width) * 100;
|
|
const paneIndex =
|
|
_.findIndex(state.panes, ({ name }) => paneName === name);
|
|
const currentPane = state.panesByName[paneName];
|
|
const rightPane =
|
|
state.panesByName[getPaneName(state.panes, paneIndex + 1)] || {};
|
|
const leftPane =
|
|
state.panesByName[getPaneName(state.panes, paneIndex - 1)] || {};
|
|
const rightBound = (rightPane.dividerLeft || 100) - dividerBuffer;
|
|
const leftBound = (leftPane.dividerLeft || 0) + dividerBuffer;
|
|
const newPosition = _.clamp(
|
|
(clientX / width) * 100,
|
|
leftBound,
|
|
rightBound
|
|
);
|
|
return {
|
|
...state,
|
|
panesByName: {
|
|
...state.panesByName,
|
|
[currentPane.name]: {
|
|
...currentPane,
|
|
dividerLeft: newPosition
|
|
}
|
|
}
|
|
};
|
|
},
|
|
[types.mouseReleased]: state => ({ ...state, pressedDivider: null }),
|
|
[types.windowResized]: (state, { payload: { height, width } }) => ({
|
|
...state,
|
|
height,
|
|
width
|
|
}),
|
|
// used to clear bin buttons
|
|
[types.panesWillUnmount]: state => ({
|
|
...state,
|
|
panes: [],
|
|
panesByName: {},
|
|
pressedDivider: null
|
|
}),
|
|
[types.updateNavHeight]: (state, { payload: navHeight }) => ({
|
|
...state,
|
|
navHeight
|
|
})
|
|
}),
|
|
initialState,
|
|
),
|
|
function metaReducer(state = initialState, action) {
|
|
if (action.meta && action.meta.panesView) {
|
|
const panesView = action.meta.panesView;
|
|
const panes = _.map(panesView, ({ name }, type) => ({ name, type }));
|
|
const numOfPanes = Object.keys(panes).length;
|
|
return {
|
|
...state,
|
|
panes,
|
|
panesByName: panes.reduce((panes, { name }, index) => {
|
|
const dividerLeft = getDividerLeft(numOfPanes, index);
|
|
panes[name] = {
|
|
name,
|
|
dividerLeft,
|
|
isHidden: false
|
|
};
|
|
return panes;
|
|
}, {})
|
|
};
|
|
}
|
|
if (action.meta && action.meta.isPaneAction) {
|
|
const name = action.meta.paneName;
|
|
const oldPane = state.panesByName[name];
|
|
const pane = {
|
|
...oldPane,
|
|
isHidden: !oldPane.isHidden
|
|
};
|
|
const panesByName = {
|
|
...state.panesByName,
|
|
[name]: pane
|
|
};
|
|
const numOfPanes = state.panes.reduce((sum, { name }) => {
|
|
return panesByName[name].isHidden ? sum : sum + 1;
|
|
}, 0);
|
|
let numOfHidden = 0;
|
|
return {
|
|
...state,
|
|
panesByName: state.panes.reduce(
|
|
(panesByName, { name }, index) => {
|
|
if (!panesByName[name].isHidden) {
|
|
const dividerLeft = getDividerLeft(
|
|
numOfPanes,
|
|
index - numOfHidden
|
|
);
|
|
panesByName[name] = {
|
|
...panesByName[name],
|
|
dividerLeft
|
|
};
|
|
} else {
|
|
numOfHidden = numOfHidden + 1;
|
|
}
|
|
return panesByName;
|
|
},
|
|
panesByName
|
|
)
|
|
};
|
|
}
|
|
return state;
|
|
}
|
|
);
|
|
|
|
return {
|
|
reducer,
|
|
middleware
|
|
};
|
|
}
|