feat: react challenges (#16099)
* chore(packages): Update redux utils * feat(Panes): Invert control of panes map creation * feat(Modern): Add view * feat(Panes): Decouple panes from Challenges * fix(Challenges): Decouple challenge views from panes map * fix(Challenge/views): PanesMap => mapStateToPanesMap This clarifies what these functions are doing * fix(Challenges): Add view type * fix(Panes): Remove unneeded panes container * feat(Panes): Invert control of pane content render This decouples the Panes from the content they render, allowing for greater flexibility. * feat(Modern): Add side panel This is common between modern and classic * feat(seed): Array to string file content * fix(files): Modern files should be polyvinyls * feat(Modern): Create editors per file * fix(seed/React): Incorrect keyfile name * feat(Modern): Highligh jsx correctly This adds highlighting for jsx. Unfortunately, this disables linting for non-javascript files as jshint will only work for those * feat(rechallenge): Add jsx ext to babel transformer * feat(seed): Normalize challenge files head/tail/content * refactor(rechallenge/build): Rename function * fix(code-storage): Pull in files from localStorage * feat(Modern/React): Add Enzyme to test runner This enables testing of React challenges * feat(Modern): Add submission type * refactor(Panes): Rename panes map update action
This commit is contained in:
committed by
Quincy Larson
parent
8b9be0242a
commit
dced96da8e
@ -3,7 +3,7 @@ import { Scheduler, Observable } from 'rx';
|
||||
import { ofType } from 'redux-epic';
|
||||
|
||||
import {
|
||||
buildClassic,
|
||||
buildFromFiles,
|
||||
buildBackendChallenge
|
||||
} from '../utils/build.js';
|
||||
import {
|
||||
@ -41,7 +41,7 @@ export default function executeChallengeEpic(actions, { getState }) {
|
||||
.map(frameTests)
|
||||
.startWith(initOutput('// running test'));
|
||||
}
|
||||
return buildClassic(files, required, shouldProxyConsole)
|
||||
return buildFromFiles(files, required, shouldProxyConsole)
|
||||
.flatMap(payload => {
|
||||
const actions = [
|
||||
frameMain(payload)
|
||||
|
@ -3,6 +3,8 @@ import { ofType } from 'redux-epic';
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import loopProtect from 'loop-protect';
|
||||
/* eslint-enable import/no-unresolved */
|
||||
import { ShallowWrapper, ReactWrapper } from 'enzyme';
|
||||
import Adapter15 from 'enzyme-adapter-react-15';
|
||||
import {
|
||||
types,
|
||||
|
||||
@ -80,6 +82,19 @@ function frameTests({ build, sources, checkChallengePayload } = {}, document) {
|
||||
const { frame: tests } = getFrameDocument(document, testId);
|
||||
refreshFrame(tests);
|
||||
tests.Rx = Rx;
|
||||
// add enzyme
|
||||
// TODO: do programatically
|
||||
// TODO: webpack lazyload this
|
||||
tests.Enzyme = {
|
||||
shallow: (node, options) => new ShallowWrapper(node, null, {
|
||||
...options,
|
||||
adapter: new Adapter15()
|
||||
}),
|
||||
mount: (node, options) => new ReactWrapper(node, null, {
|
||||
...options,
|
||||
adapter: new Adapter15()
|
||||
})
|
||||
};
|
||||
// default for classic challenges
|
||||
// should not be used for modern
|
||||
tests.__source = sources['index'] || '';
|
||||
|
@ -9,6 +9,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
var source = document.__source;
|
||||
var __getUserInput = document.__getUserInput || (x => x);
|
||||
var checkChallengePayload = document.__checkChallengePayload;
|
||||
if (document.Enzyme) {
|
||||
window.Enzyme = document.Enzyme;
|
||||
}
|
||||
|
||||
document.__getJsOutput = function getJsOutput() {
|
||||
if (window.__err || !common.shouldRun()) {
|
||||
|
@ -1,8 +1,4 @@
|
||||
import cond from 'lodash/cond';
|
||||
import identity from 'lodash/identity';
|
||||
import matchesProperty from 'lodash/matchesProperty';
|
||||
import stubTrue from 'lodash/stubTrue';
|
||||
import conforms from 'lodash/conforms';
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as babel from 'babel-core';
|
||||
import presetEs2015 from 'babel-preset-es2015';
|
||||
@ -14,7 +10,8 @@ import loopProtect from 'loop-protect';
|
||||
|
||||
import {
|
||||
transformHeadTailAndContents,
|
||||
setContent
|
||||
setContent,
|
||||
setExt
|
||||
} from '../../common/utils/polyvinyl.js';
|
||||
import castToObservable from '../../common/app/utils/cast-to-observable.js';
|
||||
|
||||
@ -30,12 +27,12 @@ loopProtect.hit = function hit(line) {
|
||||
|
||||
// const sourceReg =
|
||||
// /(<!-- fcc-start-source -->)([\s\S]*?)(?=<!-- fcc-end-source -->)/g;
|
||||
const HTML$JSReg = /html|js/;
|
||||
const console$logReg = /(?:\b)console(\.log\S+)/g;
|
||||
const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
|
||||
|
||||
const testHTMLJS = conforms({ ext: (ext) => HTML$JSReg.test(ext) });
|
||||
const testJS = matchesProperty('ext', 'js');
|
||||
const isJS = _.matchesProperty('ext', 'js');
|
||||
const testHTMLJS = _.overSome(isJS, _.matchesProperty('ext', 'html'));
|
||||
const testJS$JSX = _.overSome(isJS, _.matchesProperty('ext', 'jsx'));
|
||||
|
||||
// if shouldProxyConsole then we change instances of console log
|
||||
// to `window.__console.log`
|
||||
@ -51,7 +48,7 @@ export function proxyLoggerTransformer(file) {
|
||||
);
|
||||
}
|
||||
|
||||
export const addLoopProtect = cond([
|
||||
export const addLoopProtect = _.cond([
|
||||
[
|
||||
testHTMLJS,
|
||||
function(file) {
|
||||
@ -63,33 +60,33 @@ export const addLoopProtect = cond([
|
||||
return setContent(loopProtect(file.contents), file);
|
||||
}
|
||||
],
|
||||
[ stubTrue, identity ]
|
||||
[ _.stubTrue, _.identity ]
|
||||
]);
|
||||
export const replaceNBSP = cond([
|
||||
export const replaceNBSP = _.cond([
|
||||
[
|
||||
testHTMLJS,
|
||||
function(file) {
|
||||
return setContent(
|
||||
file.contents.replace(NBSPReg, ' '),
|
||||
file.contents.replace(NBSPReg, ' '),
|
||||
file
|
||||
);
|
||||
);
|
||||
}
|
||||
],
|
||||
[ stubTrue, identity ]
|
||||
[ _.stubTrue, _.identity ]
|
||||
]);
|
||||
|
||||
export const babelTransformer = cond([
|
||||
export const babelTransformer = _.cond([
|
||||
[
|
||||
testJS,
|
||||
testJS$JSX,
|
||||
function(file) {
|
||||
const result = babel.transform(file.contents, babelOptions);
|
||||
return setContent(
|
||||
result.code,
|
||||
file
|
||||
);
|
||||
return _.flow(
|
||||
_.partial(setContent, result.code),
|
||||
_.partial(setExt, 'js')
|
||||
)(file);
|
||||
}
|
||||
],
|
||||
[ stubTrue, identity ]
|
||||
[ _.stubTrue, _.identity ]
|
||||
]);
|
||||
|
||||
export const _transformers = [
|
||||
|
@ -35,7 +35,7 @@ const globalRequires = [
|
||||
jQuery
|
||||
];
|
||||
|
||||
export function buildClassic(files, required, shouldProxyConsole) {
|
||||
export function buildFromFiles(files, required, shouldProxyConsole) {
|
||||
const finalRequires = [...globalRequires, ...required ];
|
||||
return createFileStream(files)
|
||||
::pipe(throwers)
|
||||
|
@ -1,34 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Panes from './Panes.jsx';
|
||||
import { panesMounted } from './redux';
|
||||
|
||||
const mapStateToProps = null;
|
||||
const mapDispatchToProps = {
|
||||
panesMounted
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
nameToComponent: PropTypes.object.isRequired,
|
||||
panesMounted: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export class PanesContainer extends PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.panesMounted();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Panes { ...this.props } />
|
||||
);
|
||||
}
|
||||
}
|
||||
PanesContainer.displayName = 'PanesContainer';
|
||||
PanesContainer.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(PanesContainer);
|
@ -6,6 +6,7 @@ import { createSelector } from 'reselect';
|
||||
import {
|
||||
panesSelector,
|
||||
panesByNameSelector,
|
||||
panesMounted,
|
||||
heightSelector,
|
||||
widthSelector
|
||||
} from './redux';
|
||||
@ -38,23 +39,25 @@ const mapStateToProps = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const mapDispatchToProps = null;
|
||||
const mapDispatchToProps = { panesMounted };
|
||||
|
||||
const propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
nameToComponent: PropTypes.object.isRequired,
|
||||
panes: PropTypes.array
|
||||
panes: PropTypes.array,
|
||||
panesMounted: PropTypes.func.isRequired,
|
||||
render: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export class Panes extends PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.panesMounted();
|
||||
}
|
||||
renderPanes() {
|
||||
const {
|
||||
nameToComponent,
|
||||
render,
|
||||
panes
|
||||
} = this.props;
|
||||
return panes.map(({ name, left, right, dividerLeft }) => {
|
||||
const { Component } = nameToComponent[name] || {};
|
||||
const FinalComponent = Component ? Component : 'span';
|
||||
const divider = dividerLeft ?
|
||||
(
|
||||
<Divider
|
||||
@ -71,7 +74,7 @@ export class Panes extends PureComponent {
|
||||
left={ left }
|
||||
right={ right }
|
||||
>
|
||||
<FinalComponent />
|
||||
{ render(name) }
|
||||
</Pane>,
|
||||
divider
|
||||
];
|
||||
|
@ -1 +1 @@
|
||||
export default from './Panes-Container.jsx';
|
||||
export default from './Panes.jsx';
|
||||
|
@ -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);
|
||||
|
@ -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 || {};
|
||||
|
@ -55,7 +55,7 @@ export default function createApp({
|
||||
const {
|
||||
reducer: panesReducer,
|
||||
middleware: panesMiddleware
|
||||
} = createPanesAspects(createPanesMap());
|
||||
} = createPanesAspects({ createPanesMap });
|
||||
|
||||
const enhancer = compose(
|
||||
addLangToRoutesEnhancer(routesMap),
|
||||
|
@ -1,7 +1 @@
|
||||
import { createPanesMap as routesPanes } from './routes/';
|
||||
|
||||
export default function createPanesMap() {
|
||||
return {
|
||||
...routesPanes()
|
||||
};
|
||||
}
|
||||
export { createPanesMap as default } from './routes/';
|
||||
|
@ -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 &&
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
@ -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 = (
|
||||
<Tooltip id='tooltip'>
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
() => ({
|
||||
|
@ -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
|
||||
|
146
common/app/routes/Challenges/views/Modern/Editor.jsx
Normal file
146
common/app/routes/Challenges/views/Modern/Editor.jsx
Normal file
@ -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 (
|
||||
<div
|
||||
className={ `${ns}-editor` }
|
||||
role='main'
|
||||
>
|
||||
<NoSSR onSSR={ <CodeMirrorSkeleton content={ content } /> }>
|
||||
<Codemirror
|
||||
onChange={ content => modernEditorUpdated(fileKey, content) }
|
||||
options={ this.createOptions({ executeChallenge, mode }) }
|
||||
ref='editor'
|
||||
value={ content }
|
||||
/>
|
||||
</NoSSR>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Editor.displayName = 'Editor';
|
||||
Editor.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Editor);
|
93
common/app/routes/Challenges/views/Modern/Show.jsx
Normal file
93
common/app/routes/Challenges/views/Modern/Show.jsx
Normal file
@ -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 (
|
||||
<ChildContainer isFullWidth={ true }>
|
||||
<Panes
|
||||
render={ name => {
|
||||
const Comp = nameToComponent[name];
|
||||
if (Comp) {
|
||||
return <Comp />;
|
||||
}
|
||||
if (nameToFileKey[name]) {
|
||||
return <Editor fileKey={ nameToFileKey[name] } />;
|
||||
}
|
||||
return <span>Could not find Component for { name }</span>;
|
||||
}}
|
||||
/>
|
||||
</ChildContainer>
|
||||
);
|
||||
}
|
||||
|
||||
ShowModern.displayName = 'ShowModern';
|
||||
ShowModern.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ShowModern);
|
1
common/app/routes/Challenges/views/Modern/index.js
Normal file
1
common/app/routes/Challenges/views/Modern/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default, mapStateToPanes } from './Show.jsx';
|
1
common/app/routes/Challenges/views/Modern/ns.json
Normal file
1
common/app/routes/Challenges/views/Modern/ns.json
Normal file
@ -0,0 +1 @@
|
||||
"modern"
|
@ -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 ? <Comp /> : <span>Pane { name } not found</span>;
|
||||
};
|
||||
|
||||
export default function ShowBackEnd() {
|
||||
return (
|
||||
<ChildContainer isFullWidth={ true }>
|
||||
<Panes nameToComponent={ nameToComponentDef } />
|
||||
<Panes render={ renderPane } />
|
||||
</ChildContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { default, panesMap } from './Show.jsx';
|
||||
export { default, mapStateToPanes } from './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 ? <Comp /> : <span>Pane for { name } not found</span>;
|
||||
};
|
||||
|
||||
export default function ShowClassic() {
|
||||
return (
|
||||
<ChildContainer isFullWidth={ true }>
|
||||
<Panes nameToComponent={ nameToComponent }/>
|
||||
<Panes render={ renderPane }/>
|
||||
</ChildContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { default, panesMap } from './Show.jsx';
|
||||
export { default, mapStateToPanes } from './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 ? <Comp /> : <span>Pane { name } not found</span>;
|
||||
};
|
||||
|
||||
export default function ShowProject() {
|
||||
return (
|
||||
<ChildContainer isFullWidth={ true }>
|
||||
<Panes nameToComponent={ nameToComponent }/>
|
||||
<Panes render={ renderPane }/>
|
||||
</ChildContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { default, panesMap } from './Show.jsx';
|
||||
export { default, mapStateToPanes } from './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 ? <Comp /> : <span>Pane { name } not found</span>;
|
||||
};
|
||||
|
||||
export default function ShowQuiz() {
|
||||
return (
|
||||
<ChildContainer isFullWidth={ true }>
|
||||
<Panes nameToComponent={ nameToComponent }/>
|
||||
<Panes render={ renderPane }/>
|
||||
</ChildContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { default, panesMap } from './Show.jsx';
|
||||
export { default, mapStateToPanes } from './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 ? <Comp /> : <span>Pane { name } not found</span>;
|
||||
};
|
||||
|
||||
export default function ShowStep() {
|
||||
return (
|
||||
<ChildContainer isFullWidth={ true }>
|
||||
<Panes nameToComponent={ nameToComponent }/>
|
||||
<Panes render={ renderPane }/>
|
||||
</ChildContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export { default, panesMap } from './Show.jsx';
|
||||
export { default, mapStateToPanes } from './Show.jsx';
|
||||
|
@ -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'),
|
||||
|
272
package-lock.json
generated
272
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 <code>h1</code> element.');",
|
||||
"assert(Enzyme.shallow(jsx).children() === 'Hello JSX!', 'message: The <code>h1</code> tag should include the text <code>Hello JSX!</code>');"
|
||||
"assert(Enzyme.shallow(jsx).type() === 'h1', 'message: The constant JSX should return an <code>h1</code> element.');",
|
||||
"assert(Enzyme.shallow(jsx).contains( 'Hello JSX!'), 'message: The <code>h1</code> tag should include the text <code>Hello JSX!</code>');"
|
||||
],
|
||||
"type": "modern",
|
||||
"isRequired": true,
|
||||
"isRequired": false,
|
||||
"translations": {}
|
||||
}
|
||||
]
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user