-
+ { render(name) }
,
divider
];
diff --git a/common/app/Panes/index.js b/common/app/Panes/index.js
index a6ad764581..2d89e14578 100644
--- a/common/app/Panes/index.js
+++ b/common/app/Panes/index.js
@@ -1 +1 @@
-export default from './Panes-Container.jsx';
+export default from './Panes.jsx';
diff --git a/common/app/Panes/redux/index.js b/common/app/Panes/redux/index.js
index d5254d5cb3..df41e3a7f0 100644
--- a/common/app/Panes/redux/index.js
+++ b/common/app/Panes/redux/index.js
@@ -1,5 +1,5 @@
-import { isLocationAction } from 'redux-first-router';
import _ from 'lodash';
+import invariant from 'invariant';
import {
composeReducers,
createAction,
@@ -11,17 +11,14 @@ 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',
+ 'panesMapUpdated',
'panesMounted',
'panesUpdated',
'panesWillMount',
@@ -38,10 +35,10 @@ export const types = createTypes([
'hidePane'
], ns);
-export const panesUpdatedThroughFetch = createAction(
- types.panesUpdatedThroughFetch,
+export const panesMapUpdated = createAction(
+ types.panesMapUpdated,
null,
- panesView => ({ panesView })
+ (type, panesMap) => ({ trigger: type, panesMap })
);
export const panesMounted = createAction(types.panesMounted);
export const panesUpdated = createAction(types.panesUpdated);
@@ -57,14 +54,14 @@ export const windowResized = createAction(types.windowResized);
export const updateNavHeight = createAction(types.updateNavHeight);
export const hidePane = createAction(types.hidePane);
-const initialState = {
+const defaultState = {
height: 600,
width: 800,
navHeight: 50,
panes: [],
panesByName: {},
pressedDivider: null,
- nameToType: {}
+ panesMap: {}
};
export const getNS = state => state[ns];
export const heightSelector = state => {
@@ -77,10 +74,10 @@ 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;
+export const panesMapSelector = state => getNS(state).panesMap;
-function isPanesAction({ type } = {}, typeToName) {
- return !!typeToName[type];
+function isPanesAction({ type } = {}, panesMap) {
+ return !!panesMap[type];
}
function getDividerLeft(numOfPanes, index) {
@@ -91,98 +88,58 @@ function getDividerLeft(numOfPanes, index) {
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 checkForTypeKeys(panesMap) {
+ _.forEach(panesMap, (_, actionType) => {
+ invariant(
+ actionType !== 'undefined',
+ `action type for ${panesMap[actionType]} is undefined`
+ );
});
-}
-
-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);
+ return panesMap;
}
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`
- );
- }
- });
+function normalizePanesMapCreator(createPanesMap) {
+ invariant(
+ _.isFunction(createPanesMap),
+ 'createPanesMap should be a function but got %s',
+ createPanesMap
+ );
+ const panesMap = createPanesMap({}, { type: '@@panes/test' });
+ if (typeof panesMap === 'function') {
+ return normalizePanesMapCreator(panesMap);
}
- const typeToName = reduceConfig(config, (acc, val, type) => {
- const name = _.isObject(val) ? val.name : val;
- acc[type] = name;
- return acc;
- });
+ invariant(
+ !panesMap,
+ 'panesMap test should return undefined or null on test action but got %s',
+ panesMap
+ );
+ return createPanesMap;
+}
+
+export default function createPanesAspects({ createPanesMap }) {
+ createPanesMap = normalizePanesMapCreator(createPanesMap);
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)) {
+ const panesMap = panesMapSelector(getState());
+ if (isPanesAction(action, panesMap)) {
finalAction = {
...action,
meta: {
...action.meta,
isPaneAction: true,
- paneName: typeToName[action.type]
+ paneName: panesMap[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)));
+ const nextPanesMap = createPanesMap(getState(), action);
+ if (nextPanesMap) {
+ checkForTypeKeys(nextPanesMap);
+ next(panesMapUpdated(action.type, nextPanesMap));
}
return result;
};
@@ -242,15 +199,16 @@ export default function createPanesAspects(config) {
navHeight
})
}),
- initialState,
+ defaultState,
),
- function metaReducer(state = initialState, action) {
- if (action.meta && action.meta.panesView) {
- const panesView = action.meta.panesView;
- const panes = _.map(panesView, ({ name }, type) => ({ name, type }));
+ function metaReducer(state = defaultState, action) {
+ if (action.meta && action.meta.panesMap) {
+ const panesMap = action.meta.panesMap;
+ const panes = _.map(panesMap, (name, type) => ({ name, type }));
const numOfPanes = Object.keys(panes).length;
return {
...state,
+ panesMap,
panes,
panesByName: panes.reduce((panes, { name }, index) => {
const dividerLeft = getDividerLeft(numOfPanes, index);
diff --git a/common/app/Router/redux/index.js b/common/app/Router/redux/index.js
index 2959893d1f..f66aa02859 100644
--- a/common/app/Router/redux/index.js
+++ b/common/app/Router/redux/index.js
@@ -1,5 +1,7 @@
import { selectLocationState } from 'redux-first-router';
export const paramsSelector = state => selectLocationState(state).payload || {};
+export const locationTypeSelector =
+ state => selectLocationState(state).type || '';
export const langSelector = state => paramsSelector(state).lang || 'en';
export const routesMapSelector = state => paramsSelector(state).routesMap || {};
diff --git a/common/app/create-app.jsx b/common/app/create-app.jsx
index 0cd505ac8f..b627e3e217 100644
--- a/common/app/create-app.jsx
+++ b/common/app/create-app.jsx
@@ -55,7 +55,7 @@ export default function createApp({
const {
reducer: panesReducer,
middleware: panesMiddleware
- } = createPanesAspects(createPanesMap());
+ } = createPanesAspects({ createPanesMap });
const enhancer = compose(
addLangToRoutesEnhancer(routesMap),
diff --git a/common/app/create-panes-map.js b/common/app/create-panes-map.js
index 5ce26a90d0..79ade7fe31 100644
--- a/common/app/create-panes-map.js
+++ b/common/app/create-panes-map.js
@@ -1,7 +1 @@
-import { createPanesMap as routesPanes } from './routes/';
-
-export default function createPanesMap() {
- return {
- ...routesPanes()
- };
-}
+export { createPanesMap as default } from './routes/';
diff --git a/common/app/files/index.js b/common/app/files/index.js
index ddf8965b1e..3b0a0ca4b6 100644
--- a/common/app/files/index.js
+++ b/common/app/files/index.js
@@ -1,3 +1,4 @@
+import _ from 'lodash';
import {
combineActions,
createAction,
@@ -27,6 +28,10 @@ export const savedCodeFound = createAction(
);
export const filesSelector = state => state[ns];
+export const createFileSelector = keySelector => (state, props) => {
+ const files = filesSelector(state);
+ return files[keySelector(state, props)] || {};
+};
export default handleActions(
() => ({
@@ -42,9 +47,12 @@ export default handleActions(
}, { ...state });
},
[types.savedCodeFound]: (state, { payload: { files, challenge } }) => {
- if (challenge.type === 'mod') {
+ if (challenge.type === 'modern') {
// this may need to change to update head/tail
- return challenge.files;
+ return _.reduce(files, (files, file) => {
+ files[file.key] = createPoly(file);
+ return files;
+ }, {});
}
if (
challenge.challengeType !== html &&
@@ -70,8 +78,11 @@ export default handleActions(
app.fetchChallenge.complete
)
]: (state, { payload: { challenge } }) => {
- if (challenge.type === 'mod') {
- return challenge.files;
+ if (challenge.type === 'modern') {
+ return _.reduce(challenge.files, (files, file) => {
+ files[file.key] = createPoly(file);
+ return files;
+ }, {});
}
if (
challenge.challengeType !== html &&
diff --git a/common/app/routes/Challenges/Show.jsx b/common/app/routes/Challenges/Show.jsx
index b59dd7dce1..515e013db0 100644
--- a/common/app/routes/Challenges/Show.jsx
+++ b/common/app/routes/Challenges/Show.jsx
@@ -11,6 +11,7 @@ import Step from './views/step';
import Project from './views/project';
import BackEnd from './views/backend';
import Quiz from './views/quiz';
+import Modern from './views/Modern';
import {
fetchChallenge,
@@ -23,10 +24,11 @@ import { paramsSelector } from '../../Router/redux';
const views = {
backend: BackEnd,
classic: Classic,
+ modern: Modern,
project: Project,
+ quiz: Quiz,
simple: Project,
- step: Step,
- quiz: Quiz
+ step: Step
};
const mapDispatchToProps = {
diff --git a/common/app/routes/Challenges/views/classic/Side-Panel.jsx b/common/app/routes/Challenges/Side-Panel.jsx
similarity index 89%
rename from common/app/routes/Challenges/views/classic/Side-Panel.jsx
rename to common/app/routes/Challenges/Side-Panel.jsx
index da57345bb9..a40075e7c6 100644
--- a/common/app/routes/Challenges/views/classic/Side-Panel.jsx
+++ b/common/app/routes/Challenges/Side-Panel.jsx
@@ -7,12 +7,12 @@ import PureComponent from 'react-pure-render/component';
import ns from './ns.json';
-import BugModal from '../../Bug-Modal.jsx';
+import BugModal from './Bug-Modal.jsx';
import ToolPanel from './Tool-Panel.jsx';
-import ChallengeTitle from '../../Challenge-Title.jsx';
-import ChallengeDescription from '../../Challenge-Description.jsx';
-import TestSuite from '../../Test-Suite.jsx';
-import Output from '../../Output.jsx';
+import ChallengeTitle from './Challenge-Title.jsx';
+import ChallengeDescription from './Challenge-Description.jsx';
+import TestSuite from './Test-Suite.jsx';
+import Output from './Output.jsx';
import {
openBugModal,
updateHint,
@@ -25,11 +25,11 @@ import {
hintIndexSelector,
codeLockedSelector,
chatRoomSelector
-} from '../../redux';
+} from './redux';
-import { descriptionRegex } from '../../utils';
-import { challengeSelector } from '../../../../redux';
-import { makeToast } from '../../../../Toasts/redux';
+import { descriptionRegex } from './utils';
+import { challengeSelector } from '../../redux';
+import { makeToast } from '../../Toasts/redux';
const mapDispatchToProps = {
makeToast,
diff --git a/common/app/routes/Challenges/views/classic/Tool-Panel.jsx b/common/app/routes/Challenges/Tool-Panel.jsx
similarity index 97%
rename from common/app/routes/Challenges/views/classic/Tool-Panel.jsx
rename to common/app/routes/Challenges/Tool-Panel.jsx
index 9aa5b754bc..22f5c0efb6 100644
--- a/common/app/routes/Challenges/views/classic/Tool-Panel.jsx
+++ b/common/app/routes/Challenges/Tool-Panel.jsx
@@ -1,7 +1,6 @@
-import React from 'react';
+import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Tooltip, OverlayTrigger } from 'react-bootstrap';
-import PureComponent from 'react-pure-render/component';
const unlockWarning = (
diff --git a/common/app/routes/Challenges/index.js b/common/app/routes/Challenges/index.js
index debe8352ed..00edf41575 100644
--- a/common/app/routes/Challenges/index.js
+++ b/common/app/routes/Challenges/index.js
@@ -1,11 +1,15 @@
-import { redirect } from 'redux-first-router';
+import _ from 'lodash';
+import { isLocationAction, redirect } from 'redux-first-router';
-import { types } from './redux';
-import { panesMap as backendPanesMap } from './views/backend';
-import { panesMap as classicPanesMap } from './views/classic';
-import { panesMap as stepPanesMap } from './views/step';
-import { panesMap as projectPanesMap } from './views/project';
-import { panesMap as quizPanesMap } from './views/quiz';
+import { types, challengeMetaSelector } from './redux';
+import { mapStateToPanes as backendPanesMap } from './views/backend';
+import { mapStateToPanes as classicPanesMap } from './views/classic';
+import { mapStateToPanes as stepPanesMap } from './views/step';
+import { mapStateToPanes as projectPanesMap } from './views/project';
+import { mapStateToPanes as quizPanesMap } from './views/quiz';
+import { mapStateToPanes as modernPanesMap } from './views/Modern';
+import { types as app } from '../../redux';
+import { locationTypeSelector } from '../../Router/redux';
export const routes = {
[types.onRouteChallengeRoot]: {
@@ -18,15 +22,43 @@ export const routes = {
};
export function createPanesMap() {
- return {
- // the route to use this panes map on
- [types.onRouteChallenges]: {
- [backendPanesMap]: backendPanesMap,
- [classicPanesMap]: classicPanesMap,
- [stepPanesMap]: stepPanesMap,
- [projectPanesMap]: projectPanesMap,
- [quizPanesMap]: quizPanesMap
+ const viewMap = {
+ [backendPanesMap]: backendPanesMap,
+ [classicPanesMap]: classicPanesMap,
+ [stepPanesMap]: stepPanesMap,
+ [projectPanesMap]: projectPanesMap,
+ [quizPanesMap]: quizPanesMap,
+ [modernPanesMap]: modernPanesMap
+ };
+ return (state, action) => {
+ // if a location action has dispatched then we must update the panesmap
+ if (isLocationAction(action)) {
+ let finalPanesMap = {};
+ // if we are on this route,
+ // then we must figure out the currect view we are on
+ // this depends on the type of challenge
+ if (action.type === types.onRouteChallenges) {
+ // location matches a panes route
+ const meta = challengeMetaSelector(state);
+ // if challenge data has not been fetched yet (as in the case of SSR)
+ // then we will get a pojo factory
+ const mapStateToPanes = viewMap[meta.viewType] || _.stubObject;
+ finalPanesMap = mapStateToPanes(state);
+ }
+ return finalPanesMap;
}
+ // This should normally happen during SSR
+ // here we are ensured that the challenge data has been fetched
+ // now we can select the appropriate panes map factory
+ if (
+ action.type === app.fetchChallenge.complete &&
+ locationTypeSelector(state) === types.onRouteChallenges
+ ) {
+ const meta = challengeMetaSelector(state);
+ const mapStateToPanes = viewMap[meta.viewType] || _.stubObject;
+ return mapStateToPanes(state);
+ }
+ return null;
};
}
diff --git a/common/app/routes/Challenges/redux/editor-epic.js b/common/app/routes/Challenges/redux/editor-epic.js
index c5967fe981..6d44df1e0f 100644
--- a/common/app/routes/Challenges/redux/editor-epic.js
+++ b/common/app/routes/Challenges/redux/editor-epic.js
@@ -1,4 +1,4 @@
-import { ofType } from 'redux-epic';
+import { ofType, combineEpics } from 'redux-epic';
import {
types,
@@ -7,7 +7,7 @@ import {
import { updateFile } from '../../../files';
-export default function editorEpic(actions, { getState }) {
+export function classicEditorEpic(actions, { getState }) {
return actions::ofType(types.classicEditorUpdated)
.pluck('payload')
.map(content => updateFile({
@@ -15,3 +15,11 @@ export default function editorEpic(actions, { getState }) {
key: keySelector(getState())
}));
}
+
+export function modernEditorEpic(actions) {
+ return actions::ofType(types.modernEditorUpdated)
+ .pluck('payload')
+ .map(updateFile);
+}
+
+export default combineEpics(classicEditorEpic, modernEditorEpic);
diff --git a/common/app/routes/Challenges/redux/index.js b/common/app/routes/Challenges/redux/index.js
index 68265b6f64..604b6c758f 100644
--- a/common/app/routes/Challenges/redux/index.js
+++ b/common/app/routes/Challenges/redux/index.js
@@ -57,6 +57,8 @@ export const types = createTypes([
'unlockUntrustedCode',
'closeChallengeModal',
'updateSuccessMessage',
+ // |- modern
+ 'modernEditorUpdated',
// rechallenge
'executeChallenge',
@@ -83,7 +85,8 @@ export const types = createTypes([
'toggleMap',
'togglePreview',
'toggleSidePanel',
- 'toggleStep'
+ 'toggleStep',
+ 'toggleModernEditor'
], ns);
// routes
@@ -93,6 +96,11 @@ export const onRouteCurrentChallenge =
// classic
export const classicEditorUpdated = createAction(types.classicEditorUpdated);
+// modern
+export const modernEditorUpdated = createAction(
+ types.modernEditorUpdated,
+ (key, content) => ({ key, content })
+);
// challenges
export const closeChallengeModal = createAction(types.closeChallengeModal);
export const updateHint = createAction(types.updateHint);
@@ -204,6 +212,9 @@ export const challengeMetaSelector = createSelector(
}
);
+export const showPreviewSelector = state =>
+ !!challengeMetaSelector(state).showPreview;
+
export default combineReducers(
handleActions(
() => ({
diff --git a/common/app/routes/Challenges/utils.js b/common/app/routes/Challenges/utils.js
index 5f04d16630..b1c4483059 100644
--- a/common/app/routes/Challenges/utils.js
+++ b/common/app/routes/Challenges/utils.js
@@ -13,7 +13,8 @@ export const viewTypes = {
[ challengeTypes.video ]: 'video',
[ challengeTypes.step ]: 'step',
[ challengeTypes.quiz ]: 'quiz',
- backend: 'backend'
+ backend: 'backend',
+ modern: 'modern'
};
// determine the type of submit function to use for the challenge on completion
@@ -34,7 +35,8 @@ export const submitTypes = {
[ challengeTypes.video ]: 'video',
[ challengeTypes.step ]: 'step',
[ challengeTypes.quiz ]: 'quiz',
- backend: 'backend'
+ backend: 'backend',
+ modern: 'tests'
};
// determines if a line in a challenge description
diff --git a/common/app/routes/Challenges/views/Modern/Editor.jsx b/common/app/routes/Challenges/views/Modern/Editor.jsx
new file mode 100644
index 0000000000..a22adf61eb
--- /dev/null
+++ b/common/app/routes/Challenges/views/Modern/Editor.jsx
@@ -0,0 +1,146 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import Codemirror from 'react-codemirror';
+import NoSSR from 'react-no-ssr';
+import MouseTrap from 'mousetrap';
+
+import ns from './ns.json';
+import CodeMirrorSkeleton from '../../Code-Mirror-Skeleton.jsx';
+import {
+ executeChallenge,
+ modernEditorUpdated,
+ challengeMetaSelector
+} from '../../redux';
+
+import { createFileSelector } from '../../../../files';
+
+const envProps = typeof window !== 'undefined' ? Object.keys(window) : [];
+const options = {
+ lint: {
+ esversion: 6,
+ predef: envProps
+ },
+ lineNumbers: true,
+ mode: 'javascript',
+ runnable: true,
+ matchBrackets: true,
+ autoCloseBrackets: true,
+ scrollbarStyle: 'null',
+ lineWrapping: true,
+ gutters: [ 'CodeMirror-lint-markers' ]
+};
+
+const mapStateToProps = createSelector(
+ createFileSelector((_, { fileKey }) => fileKey || ''),
+ challengeMetaSelector,
+ (
+ file,
+ { mode }
+ ) => ({
+ content: file.contents || '// Happy Coding!',
+ file: file,
+ mode: file.ext || mode || 'javascript'
+ })
+);
+
+const mapDispatchToProps = {
+ executeChallenge,
+ modernEditorUpdated
+};
+
+const propTypes = {
+ content: PropTypes.string,
+ executeChallenge: PropTypes.func.isRequired,
+ fileKey: PropTypes.string,
+ mode: PropTypes.string,
+ modernEditorUpdated: PropTypes.func.isRequired
+};
+
+export class Editor extends PureComponent {
+ createOptions = createSelector(
+ state => state.executeChallenge,
+ state => state.mode,
+ (executeChallenge, mode) => ({
+ ...options,
+ mode,
+ // JSHint only works with javascript
+ // we will need to switch to eslint to make this work with jsx
+ lint: mode === 'javascript' ? options.lint : false,
+ extraKeys: {
+ Esc() {
+ document.activeElement.blur();
+ },
+ Tab(cm) {
+ if (cm.somethingSelected()) {
+ return cm.indentSelection('add');
+ }
+ const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
+ return cm.replaceSelection(spaces);
+ },
+ 'Shift-Tab': function(cm) {
+ return cm.indentSelection('subtract');
+ },
+ 'Ctrl-Enter': function() {
+ executeChallenge();
+ return false;
+ },
+ 'Cmd-Enter': function() {
+ executeChallenge();
+ return false;
+ },
+ 'Ctrl-/': function(cm) {
+ cm.toggleComment();
+ },
+ 'Cmd-/': function(cm) {
+ cm.toggleComment();
+ }
+ }
+ })
+ );
+
+ componentDidMount() {
+ MouseTrap.bind('e', () => {
+ this.refs.editor.focus();
+ }, 'keyup');
+ }
+
+ componentWillUnmount() {
+ MouseTrap.unbind('e', 'keyup');
+ }
+
+ render() {
+ const {
+ content,
+ modernEditorUpdated,
+ executeChallenge,
+ fileKey,
+ mode
+ } = this.props;
+ return (
+
+ }>
+ modernEditorUpdated(fileKey, content) }
+ options={ this.createOptions({ executeChallenge, mode }) }
+ ref='editor'
+ value={ content }
+ />
+
+
+ );
+ }
+}
+
+Editor.displayName = 'Editor';
+Editor.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Editor);
diff --git a/common/app/routes/Challenges/views/Modern/Show.jsx b/common/app/routes/Challenges/views/Modern/Show.jsx
new file mode 100644
index 0000000000..cb6d2a1bd2
--- /dev/null
+++ b/common/app/routes/Challenges/views/Modern/Show.jsx
@@ -0,0 +1,93 @@
+import _ from 'lodash';
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { createSelector } from 'reselect';
+import { addNS } from 'berkeleys-redux-utils';
+
+import ns from './ns.json';
+import Editor from './Editor.jsx';
+import { showPreviewSelector, types } from '../../redux';
+import SidePanel from '../../Side-Panel.jsx';
+import Panes from '../../../../Panes';
+import _Map from '../../../../Map';
+import ChildContainer from '../../../../Child-Container.jsx';
+import { filesSelector } from '../../../../files';
+
+const createModernEditorToggleType = fileKey =>
+ types.toggleModernEditor + `(${fileKey})`;
+
+const propTypes = {
+ nameToFileKey: PropTypes.object
+};
+
+const mapStateToProps = createSelector(
+ filesSelector,
+ files => ({
+ nameToFileKey: _.reduce(files, (map, file) => {
+ map[file.name] = file.key;
+ return map;
+ }, {})
+ })
+);
+
+const mapDispatchToProps = null;
+
+export const mapStateToPanes = addNS(
+ ns,
+ createSelector(
+ filesSelector,
+ showPreviewSelector,
+ (files, showPreview)=> {
+ // create panes map here
+ // must include map
+ // side panel
+ // editors are created based on state
+ // so pane component can have multiple panes based on state
+ const panesMap = _.reduce(files, (map, file) => {
+ map[createModernEditorToggleType(file.fileKey)] = file.name;
+ return map;
+ }, {
+ [types.toggleMap]: 'Map',
+ [types.toggleSidePanel]: 'Side Panel'
+ });
+
+ if (showPreview) {
+ panesMap[types.togglePreview] = 'Preview';
+ }
+ return panesMap;
+ }
+ )
+);
+
+const nameToComponent = {
+ Map: _Map,
+ 'Side Panel': SidePanel
+};
+
+export function ShowModern({ nameToFileKey }) {
+ return (
+
+ {
+ const Comp = nameToComponent[name];
+ if (Comp) {
+ return ;
+ }
+ if (nameToFileKey[name]) {
+ return ;
+ }
+ return Could not find Component for { name };
+ }}
+ />
+
+ );
+}
+
+ShowModern.displayName = 'ShowModern';
+ShowModern.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ShowModern);
diff --git a/common/app/routes/Challenges/views/Modern/index.js b/common/app/routes/Challenges/views/Modern/index.js
new file mode 100644
index 0000000000..53c86f5f0b
--- /dev/null
+++ b/common/app/routes/Challenges/views/Modern/index.js
@@ -0,0 +1 @@
+export { default, mapStateToPanes } from './Show.jsx';
diff --git a/common/app/routes/Challenges/views/Modern/ns.json b/common/app/routes/Challenges/views/Modern/ns.json
new file mode 100644
index 0000000000..1abaf5ef09
--- /dev/null
+++ b/common/app/routes/Challenges/views/Modern/ns.json
@@ -0,0 +1 @@
+"modern"
diff --git a/common/app/routes/Challenges/views/backend/Show.jsx b/common/app/routes/Challenges/views/backend/Show.jsx
index 47a8dbc336..d90c32643c 100644
--- a/common/app/routes/Challenges/views/backend/Show.jsx
+++ b/common/app/routes/Challenges/views/backend/Show.jsx
@@ -1,15 +1,15 @@
import React from 'react';
+import { addNS } from 'berkeleys-redux-utils';
import BackEnd from './Back-End.jsx';
import { types } from '../../redux';
import Panes from '../../../../Panes';
-import { createPaneMap } from '../../../../Panes/redux';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
const propTypes = {};
-export const panesMap = createPaneMap(
+export const mapStateToPanes = addNS(
'backend',
() => ({
[types.toggleMap]: 'Map',
@@ -17,21 +17,20 @@ export const panesMap = createPaneMap(
})
);
-const nameToComponentDef = {
- Map: {
- Component: _Map,
- defaultSize: 25
- },
- Main: {
- Component: BackEnd,
- defaultSize: 50
- }
+const nameToComponent = {
+ Map: _Map,
+ Main: BackEnd
+};
+
+const renderPane = name => {
+ const Comp = nameToComponent[name];
+ return Comp ? : Pane { name } not found;
};
export default function ShowBackEnd() {
return (
-
+
);
}
diff --git a/common/app/routes/Challenges/views/backend/index.js b/common/app/routes/Challenges/views/backend/index.js
index f8a8115a06..53c86f5f0b 100644
--- a/common/app/routes/Challenges/views/backend/index.js
+++ b/common/app/routes/Challenges/views/backend/index.js
@@ -1 +1 @@
-export { default, panesMap } from './Show.jsx';
+export { default, mapStateToPanes } from './Show.jsx';
diff --git a/common/app/routes/Challenges/views/classic/Show.jsx b/common/app/routes/Challenges/views/classic/Show.jsx
index cfcb99b203..e6807028b1 100644
--- a/common/app/routes/Challenges/views/classic/Show.jsx
+++ b/common/app/routes/Challenges/views/classic/Show.jsx
@@ -1,48 +1,48 @@
import React from 'react';
+import { addNS } from 'berkeleys-redux-utils';
-import SidePanel from './Side-Panel.jsx';
import Editor from './Editor.jsx';
import Preview from './Preview.jsx';
-import { types, challengeMetaSelector } from '../../redux';
+import { types, showPreviewSelector } from '../../redux';
+import SidePanel from '../../Side-Panel.jsx';
import Panes from '../../../../Panes';
-import { createPaneMap } from '../../../../Panes/redux';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
const propTypes = {};
-export const panesMap = createPaneMap(
+export const mapStateToPanes = addNS(
'classic',
- () => ({
- [types.toggleMap]: 'Map',
- [types.toggleSidePanel]: 'Side Panel',
- [types.toggleClassicEditor]: 'Editor',
- [types.togglePreview]: {
- name: 'Preview',
- filter: state => !!challengeMetaSelector(state).showPreview
+ state => {
+ const panesMap = {
+ [types.toggleMap]: 'Map',
+ [types.toggleSidePanel]: 'Side Panel',
+ [types.toggleClassicEditor]: 'Editor'
+ };
+
+ if (showPreviewSelector(state)) {
+ panesMap[types.togglePreview] = 'Preview';
}
- })
+ return panesMap;
+ }
);
const nameToComponent = {
- Map: {
- Component: _Map
- },
- 'Side Panel': {
- Component: SidePanel
- },
- Editor: {
- Component: Editor
- },
- Preview: {
- Component: Preview
- }
+ Map: _Map,
+ 'Side Panel': SidePanel,
+ Editor: Editor,
+ Preview: Preview
+};
+
+const renderPane = name => {
+ const Comp = nameToComponent[name];
+ return Comp ? : Pane for { name } not found;
};
export default function ShowClassic() {
return (
-
+
);
}
diff --git a/common/app/routes/Challenges/views/classic/index.js b/common/app/routes/Challenges/views/classic/index.js
index f8a8115a06..53c86f5f0b 100644
--- a/common/app/routes/Challenges/views/classic/index.js
+++ b/common/app/routes/Challenges/views/classic/index.js
@@ -1 +1 @@
-export { default, panesMap } from './Show.jsx';
+export { default, mapStateToPanes } from './Show.jsx';
diff --git a/common/app/routes/Challenges/views/project/Show.jsx b/common/app/routes/Challenges/views/project/Show.jsx
index cc6fd5751e..084111449c 100644
--- a/common/app/routes/Challenges/views/project/Show.jsx
+++ b/common/app/routes/Challenges/views/project/Show.jsx
@@ -1,15 +1,15 @@
import React from 'react';
+import { addNS } from 'berkeleys-redux-utils';
import ns from './ns.json';
import Main from './Project.jsx';
import { types } from '../../redux';
import Panes from '../../../../Panes';
-import { createPaneMap } from '../../../../Panes/redux';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
const propTypes = {};
-export const panesMap = createPaneMap(
+export const mapStateToPanes = addNS(
ns,
() => ({
[types.toggleMap]: 'Map',
@@ -18,18 +18,19 @@ export const panesMap = createPaneMap(
);
const nameToComponent = {
- Map: {
- Component: _Map
- },
- Main: {
- Component: Main
- }
+ Map: _Map,
+ Main: Main
+};
+
+const renderPane = name => {
+ const Comp = nameToComponent[name];
+ return Comp ? : Pane { name } not found;
};
export default function ShowProject() {
return (
-
+
);
}
diff --git a/common/app/routes/Challenges/views/project/index.js b/common/app/routes/Challenges/views/project/index.js
index f8a8115a06..53c86f5f0b 100644
--- a/common/app/routes/Challenges/views/project/index.js
+++ b/common/app/routes/Challenges/views/project/index.js
@@ -1 +1 @@
-export { default, panesMap } from './Show.jsx';
+export { default, mapStateToPanes } from './Show.jsx';
diff --git a/common/app/routes/Challenges/views/quiz/Show.jsx b/common/app/routes/Challenges/views/quiz/Show.jsx
index 6b7fe113bb..b1ee4fd8f3 100644
--- a/common/app/routes/Challenges/views/quiz/Show.jsx
+++ b/common/app/routes/Challenges/views/quiz/Show.jsx
@@ -1,15 +1,15 @@
import React from 'react';
+import { addNS } from 'berkeleys-redux-utils';
import ns from './ns.json';
import Main from './Quiz.jsx';
import { types } from '../../redux';
import Panes from '../../../../Panes';
-import { createPaneMap } from '../../../../Panes/redux';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
const propTypes = {};
-export const panesMap = createPaneMap(
+export const mapStateToPanes = addNS(
ns,
() => ({
[types.toggleMap]: 'Map',
@@ -18,18 +18,19 @@ export const panesMap = createPaneMap(
);
const nameToComponent = {
- Map: {
- Component: _Map
- },
- Main: {
- Component: Main
- }
+ Map: _Map,
+ Main: Main
+};
+
+const renderPane = name => {
+ const Comp = nameToComponent[name];
+ return Comp ? : Pane { name } not found;
};
export default function ShowQuiz() {
return (
-
+
);
}
diff --git a/common/app/routes/Challenges/views/quiz/index.js b/common/app/routes/Challenges/views/quiz/index.js
index f8a8115a06..53c86f5f0b 100644
--- a/common/app/routes/Challenges/views/quiz/index.js
+++ b/common/app/routes/Challenges/views/quiz/index.js
@@ -1 +1 @@
-export { default, panesMap } from './Show.jsx';
+export { default, mapStateToPanes } from './Show.jsx';
diff --git a/common/app/routes/Challenges/views/step/Show.jsx b/common/app/routes/Challenges/views/step/Show.jsx
index 67fa13963f..ae17ee8ebc 100644
--- a/common/app/routes/Challenges/views/step/Show.jsx
+++ b/common/app/routes/Challenges/views/step/Show.jsx
@@ -1,15 +1,15 @@
import React from 'react';
+import { addNS } from 'berkeleys-redux-utils';
import ns from './ns.json';
import Step from './Step.jsx';
import { types } from '../../redux';
import Panes from '../../../../Panes';
-import { createPaneMap } from '../../../../Panes/redux';
import _Map from '../../../../Map';
import ChildContainer from '../../../../Child-Container.jsx';
const propTypes = {};
-export const panesMap = createPaneMap(
+export const mapStateToPanes = addNS(
ns,
() => ({
[types.toggleMap]: 'Map',
@@ -18,18 +18,19 @@ export const panesMap = createPaneMap(
);
const nameToComponent = {
- Map: {
- Component: _Map
- },
- Step: {
- Component: Step
- }
+ Map: _Map,
+ Step: Step
+};
+
+const renderPane = name => {
+ const Comp = nameToComponent[name];
+ return Comp ? : Pane { name } not found;
};
export default function ShowStep() {
return (
-
+
);
}
diff --git a/common/app/routes/Challenges/views/step/index.js b/common/app/routes/Challenges/views/step/index.js
index f8a8115a06..53c86f5f0b 100644
--- a/common/app/routes/Challenges/views/step/index.js
+++ b/common/app/routes/Challenges/views/step/index.js
@@ -1 +1 @@
-export { default, panesMap } from './Show.jsx';
+export { default, mapStateToPanes } from './Show.jsx';
diff --git a/gulpfile.js b/gulpfile.js
index 17c7621c86..9b7516fdcd 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -108,6 +108,7 @@ const paths = {
resolve('codemirror', 'lib/codemirror.js', 'addon/lint/lint.js'),
resolve('codemirror', 'lib/codemirror.js', 'addon/lint/javascript-lint.js'),
resolve('codemirror', 'lib/codemirror.js', 'mode/javascript/javascript.js'),
+ resolve('codemirror', 'lib/codemirror.js', 'mode/jsx/jsx.js'),
resolve('codemirror', 'lib/codemirror.js', 'mode/xml/xml.js'),
resolve('codemirror', 'lib/codemirror.js', 'mode/css/css.js'),
resolve('codemirror', 'lib/codemirror.js', 'mode/htmlmixed/htmlmixed.js'),
diff --git a/package-lock.json b/package-lock.json
index e04c446486..b574a3bfb4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1839,9 +1839,9 @@
"dev": true
},
"berkeleys-redux-utils": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/berkeleys-redux-utils/-/berkeleys-redux-utils-3.2.0.tgz",
- "integrity": "sha512-w6MZvum7TuVLxHoBqidlXtwdp325hEfTUx128tytV1sitHt75LnitR9evgWecV6O9IcKItx6lDNQiQkr177dBQ==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/berkeleys-redux-utils/-/berkeleys-redux-utils-4.0.0.tgz",
+ "integrity": "sha512-2ZwevXuCFPgtg5mWmuWDClP/xAGH0+2Ijm5TL42nSEqY5JGwReis4W9Ltr5q3G7G7hfLt6eS+/7igPN3rHDe9g==",
"requires": {
"babel-runtime": "6.26.0",
"invariant": "2.2.2"
@@ -1904,6 +1904,11 @@
"type-is": "1.6.15"
}
},
+ "boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
+ },
"boom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
@@ -2434,6 +2439,39 @@
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
+ "cheerio": {
+ "version": "1.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
+ "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=",
+ "requires": {
+ "css-select": "1.2.0",
+ "dom-serializer": "0.1.0",
+ "entities": "1.1.1",
+ "htmlparser2": "3.9.2",
+ "lodash": "4.17.4",
+ "parse5": "3.0.3"
+ },
+ "dependencies": {
+ "entities": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
+ "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
+ },
+ "htmlparser2": {
+ "version": "3.9.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+ "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+ "requires": {
+ "domelementtype": "1.3.0",
+ "domhandler": "2.3.0",
+ "domutils": "1.5.1",
+ "entities": "1.1.1",
+ "inherits": "2.0.3",
+ "readable-stream": "2.2.7"
+ }
+ }
+ }
+ },
"chokidar": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@@ -3429,11 +3467,27 @@
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz",
"integrity": "sha1-OLBQP7+dqfVOnB29pg4UXHcRe90="
},
+ "css-select": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
+ "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
+ "requires": {
+ "boolbase": "1.0.0",
+ "css-what": "2.1.0",
+ "domutils": "1.5.1",
+ "nth-check": "1.0.1"
+ }
+ },
"css-stringify": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz",
"integrity": "sha1-sNBClG2ylTu50pKQCmy19tASIDE="
},
+ "css-what": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz",
+ "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0="
+ },
"csurf": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz",
@@ -3733,6 +3787,11 @@
"integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=",
"dev": true
},
+ "discontinuous-range": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
+ "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo="
+ },
"dns-prefetch-control": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz",
@@ -4112,6 +4171,46 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
"integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY="
},
+ "enzyme": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.2.0.tgz",
+ "integrity": "sha512-l0HcjycivXjB4IXkwuRc1K5z8hzWIVZB2b/Y/H2bao9eFTpBz4ACOwAQf44SgG5Nu3d1jF41LasxDgFWZeeysA==",
+ "requires": {
+ "cheerio": "1.0.0-rc.2",
+ "function.prototype.name": "1.0.3",
+ "has": "1.0.1",
+ "is-subset": "0.1.1",
+ "lodash": "4.17.4",
+ "object-is": "1.0.1",
+ "object.assign": "4.0.4",
+ "object.entries": "1.0.4",
+ "object.values": "1.0.4",
+ "raf": "3.4.0",
+ "rst-selector-parser": "2.2.3"
+ }
+ },
+ "enzyme-adapter-react-15": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/enzyme-adapter-react-15/-/enzyme-adapter-react-15-1.0.5.tgz",
+ "integrity": "sha512-GxQ+ZYbo6YFwwpaLc9LLyAwsx+F1au628/+hwTx3XV2OiuvHGyWgC/r1AAK1HlDRjujzfwwMNZTc/JxkjIuYVg==",
+ "requires": {
+ "enzyme-adapter-utils": "1.2.0",
+ "lodash": "4.17.4",
+ "object.assign": "4.0.4",
+ "object.values": "1.0.4",
+ "prop-types": "15.6.0"
+ }
+ },
+ "enzyme-adapter-utils": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.2.0.tgz",
+ "integrity": "sha512-6CeIrmymLWoQgvH5m/ixJLaCsa6pSoWU2nlMeO0nHCZR8LQ+tKzP/jPh4qceTPlB4oFfyMRFeqr0+IryY4gAxg==",
+ "requires": {
+ "lodash": "4.17.4",
+ "object.assign": "4.0.4",
+ "prop-types": "15.6.0"
+ }
+ },
"errno": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz",
@@ -4147,7 +4246,6 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz",
"integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==",
- "dev": true,
"requires": {
"es-to-primitive": "1.1.1",
"function-bind": "1.1.1",
@@ -4160,7 +4258,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
"integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=",
- "dev": true,
"requires": {
"is-callable": "1.1.3",
"is-date-object": "1.0.1",
@@ -6435,6 +6532,16 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
+ "function.prototype.name": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.3.tgz",
+ "integrity": "sha512-5EblxZUdioXi2JiMZ9FUbwYj40eQ9MFHyzFLBSPdlRl3SO8l7SLWuAnQ/at/1Wi4hjJwME/C5WpF2ZfAc8nGNw==",
+ "requires": {
+ "define-properties": "1.1.2",
+ "function-bind": "1.1.1",
+ "is-callable": "1.1.3"
+ }
+ },
"functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
@@ -7534,7 +7641,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz",
"integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=",
- "dev": true,
"requires": {
"function-bind": "1.1.1"
}
@@ -8053,8 +8159,7 @@
"is-callable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
- "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=",
- "dev": true
+ "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI="
},
"is-ci": {
"version": "1.0.10",
@@ -8068,8 +8173,7 @@
"is-date-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
- "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
- "dev": true
+ "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
},
"is-dotfile": {
"version": "1.0.3",
@@ -8274,7 +8378,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
- "dev": true,
"requires": {
"has": "1.0.1"
}
@@ -8312,11 +8415,15 @@
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
+ "is-subset": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
+ "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY="
+ },
"is-symbol": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz",
- "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
- "dev": true
+ "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI="
},
"is-typedarray": {
"version": "1.0.0",
@@ -9462,6 +9569,11 @@
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
"dev": true
},
+ "lodash.flattendeep": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
+ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI="
+ },
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -10854,6 +10966,37 @@
}
}
},
+ "nearley": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.11.0.tgz",
+ "integrity": "sha512-clqqhEuP0ZCJQ85Xv2I/4o2Gs/fvSR6fCg5ZHVE2c8evWyNk2G++ih4JOO3lMb/k/09x6ihQ2nzKUlB/APCWjg==",
+ "requires": {
+ "nomnom": "1.6.2",
+ "railroad-diagrams": "1.0.0",
+ "randexp": "0.4.6"
+ },
+ "dependencies": {
+ "colors": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
+ "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q="
+ },
+ "nomnom": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz",
+ "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=",
+ "requires": {
+ "colors": "0.5.1",
+ "underscore": "1.4.4"
+ }
+ },
+ "underscore": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
+ "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
+ }
+ }
+ },
"needle": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.0.1.tgz",
@@ -11398,6 +11541,14 @@
"path-key": "2.0.1"
}
},
+ "nth-check": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
+ "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=",
+ "requires": {
+ "boolbase": "1.0.0"
+ }
+ },
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@@ -11430,6 +11581,11 @@
"integrity": "sha512-OHHnLgLNXpM++GnJRyyhbr2bwl3pPVm4YvaraHrRvDt/N3r+s/gDVHciA7EJBTkijKXj61ssgSAikq1fb0IBRg==",
"dev": true
},
+ "object-is": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz",
+ "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY="
+ },
"object-keys": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
@@ -11463,6 +11619,17 @@
"isobject": "3.0.1"
}
},
+ "object.entries": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz",
+ "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=",
+ "requires": {
+ "define-properties": "1.1.2",
+ "es-abstract": "1.9.0",
+ "function-bind": "1.1.1",
+ "has": "1.0.1"
+ }
+ },
"object.omit": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
@@ -11493,6 +11660,17 @@
"isobject": "3.0.1"
}
},
+ "object.values": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz",
+ "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=",
+ "requires": {
+ "define-properties": "1.1.2",
+ "es-abstract": "1.9.0",
+ "function-bind": "1.1.1",
+ "has": "1.0.1"
+ }
+ },
"omni-fetch": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/omni-fetch/-/omni-fetch-0.1.0.tgz",
@@ -11854,6 +12032,14 @@
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
"dev": true
},
+ "parse5": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
+ "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
+ "requires": {
+ "@types/node": "8.0.47"
+ }
+ },
"parsejson": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
@@ -12320,6 +12506,20 @@
"performance-now": "2.1.0"
}
},
+ "railroad-diagrams": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
+ "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234="
+ },
+ "randexp": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+ "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+ "requires": {
+ "discontinuous-range": "1.0.0",
+ "ret": "0.1.15"
+ }
+ },
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -12412,13 +12612,15 @@
"dev": true
},
"react": {
- "version": "15.4.2",
- "resolved": "https://registry.npmjs.org/react/-/react-15.4.2.tgz",
- "integrity": "sha1-QfeZGyYYU5K6m66WyIiefgGDl+8=",
+ "version": "15.6.2",
+ "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
+ "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
"requires": {
+ "create-react-class": "15.6.2",
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
- "object-assign": "4.1.1"
+ "object-assign": "4.1.1",
+ "prop-types": "15.6.0"
}
},
"react-addons-css-transition-group": {
@@ -12474,13 +12676,14 @@
}
},
"react-dom": {
- "version": "15.4.2",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.4.2.tgz",
- "integrity": "sha1-AVNj8FsKH9Uq6e/dOgBg2QaVII8=",
+ "version": "15.6.2",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz",
+ "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
- "object-assign": "4.1.1"
+ "object-assign": "4.1.1",
+ "prop-types": "15.6.0"
}
},
"react-fontawesome": {
@@ -12605,6 +12808,15 @@
"prop-types": "15.6.0"
}
},
+ "react-test-renderer": {
+ "version": "15.6.2",
+ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.6.2.tgz",
+ "integrity": "sha1-0DM0NPwsQ4CSaWyncNpe1IA376g=",
+ "requires": {
+ "fbjs": "0.8.16",
+ "object-assign": "4.1.1"
+ }
+ },
"react-transition-group": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz",
@@ -12795,9 +13007,9 @@
"requires": {
"debug": "2.6.9",
"invariant": "2.2.2",
- "react": "15.4.2",
+ "react": "15.6.2",
"react-addons-shallow-compare": "15.4.2",
- "react-dom": "15.4.2",
+ "react-dom": "15.6.2",
"rx": "4.1.0",
"warning": "3.0.0"
},
@@ -13141,6 +13353,11 @@
"through": "2.3.8"
}
},
+ "ret": {
+ "version": "0.1.15",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
+ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
+ },
"rev-del": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/rev-del/-/rev-del-1.0.5.tgz",
@@ -13298,6 +13515,15 @@
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
},
+ "rst-selector-parser": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
+ "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=",
+ "requires": {
+ "lodash.flattendeep": "4.4.0",
+ "nearley": "2.11.0"
+ }
+ },
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
diff --git a/package.json b/package.json
index ca3c68adc5..509793448c 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-register": "^6.3.0",
- "berkeleys-redux-utils": "^3.2.0",
+ "berkeleys-redux-utils": "^4.0.0",
"body-parser": "^1.13.2",
"bootstrap": "~3.3.7",
"cal-heatmap": "~3.5.2",
@@ -54,6 +54,8 @@
"dedent": "~0.7.0",
"dotenv": "^4.0.0",
"emmet-codemirror": "^1.2.5",
+ "enzyme": "^3.2.0",
+ "enzyme-adapter-react-15": "^1.0.5",
"errorhandler": "^1.4.2",
"es6-map": "~0.1.1",
"express": "^4.13.3",
@@ -102,12 +104,12 @@
"passport-twitter": "^1.0.3",
"pmx": "~0.6.2",
"prop-types": "^15.5.10",
- "react": "~15.4.2",
+ "react": "^15.6.2",
"react-addons-css-transition-group": "~15.4.2",
"react-addons-shallow-compare": "~15.4.2",
"react-bootstrap": "~0.31.2",
"react-codemirror": "^0.3.0",
- "react-dom": "~15.4.2",
+ "react-dom": "^15.6.2",
"react-fontawesome": "^1.2.0",
"react-images": "^0.5.1",
"react-motion": "~0.4.2",
@@ -115,6 +117,7 @@
"react-notification": "git+https://github.com/BerkeleyTrue/react-notification.git#freecodecamp",
"react-pure-render": "^1.0.2",
"react-redux": "^4.0.6",
+ "react-test-renderer": "^15.6.2",
"react-youtube": "^7.0.0",
"redux": "^3.0.5",
"redux-actions": "^2.0.3",
diff --git a/seed/challenges/03-front-end-libraries/react.json b/seed/challenges/03-front-end-libraries/react.json
index c254a3f852..fc6adf8d7a 100644
--- a/seed/challenges/03-front-end-libraries/react.json
+++ b/seed/challenges/03-front-end-libraries/react.json
@@ -3,6 +3,11 @@
"order": 5,
"time": "5 hours",
"helpRoom": "Help",
+ "required": [
+ {
+ "src": "https://cdnjs.cloudflare.com/ajax/libs/react/16.1.1/umd/react.development.js"
+ }
+ ],
"challenges": [
{
"id": "587d7dbc367417b2b2512bb1",
@@ -17,7 +22,7 @@
],
"files": {
"indexjsx": {
- "key": "indexjxs",
+ "key": "indexjsx",
"ext": "jsx",
"name": "index",
"contents": [
@@ -28,11 +33,11 @@
}
},
"tests": [
- "assert(Enzyme.shallow(jsx).type === 'h1', 'message: The constant JSX should return an h1
element.');",
- "assert(Enzyme.shallow(jsx).children() === 'Hello JSX!', 'message: The h1
tag should include the text Hello JSX!
');"
+ "assert(Enzyme.shallow(jsx).type() === 'h1', 'message: The constant JSX should return an h1
element.');",
+ "assert(Enzyme.shallow(jsx).contains( 'Hello JSX!'), 'message: The h1
tag should include the text Hello JSX!
');"
],
"type": "modern",
- "isRequired": true,
+ "isRequired": false,
"translations": {}
}
]
diff --git a/seed/index.js b/seed/index.js
index e2de5da823..e1d7f79a07 100644
--- a/seed/index.js
+++ b/seed/index.js
@@ -23,6 +23,8 @@ var createChallenges =
var Block = app.models.Block;
var destroyBlocks = Observable.fromNodeCallback(Block.destroyAll, Block);
var createBlocks = Observable.fromNodeCallback(Block.create, Block);
+const arrToString = arr =>
+ Array.isArray(arr) ? arr.join('\n') : _.toString(arr);
Observable.combineLatest(
destroyChallenges(),
@@ -82,6 +84,17 @@ Observable.combineLatest(
)
);
+ if (challenge.files) {
+ challenge.files = _.reduce(challenge.files, (map, file) => {
+ map[file.key] = {
+ ...file,
+ head: arrToString(file.head),
+ contents: arrToString(file.contents),
+ tail: arrToString(file.tail)
+ };
+ return map;
+ }, {});
+ }
challenge.fileName = fileName;
challenge.helpRoom = helpRoom;
challenge.order = order;