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:
Berkeley Martinez
2017-11-29 15:44:51 -08:00
committed by Quincy Larson
parent 8b9be0242a
commit dced96da8e
39 changed files with 796 additions and 302 deletions

View File

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

View File

@ -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'] || '';

View File

@ -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()) {

View File

@ -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 = [

View File

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

View File

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

View File

@ -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
];

View File

@ -1 +1 @@
export default from './Panes-Container.jsx';
export default from './Panes.jsx';

View File

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

View File

@ -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 || {};

View File

@ -55,7 +55,7 @@ export default function createApp({
const {
reducer: panesReducer,
middleware: panesMiddleware
} = createPanesAspects(createPanesMap());
} = createPanesAspects({ createPanesMap });
const enhancer = compose(
addLangToRoutesEnhancer(routesMap),

View File

@ -1,7 +1 @@
import { createPanesMap as routesPanes } from './routes/';
export default function createPanesMap() {
return {
...routesPanes()
};
}
export { createPanesMap as default } from './routes/';

View File

@ -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 &&

View File

@ -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 = {

View File

@ -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,

View File

@ -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'>

View File

@ -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;
};
}

View File

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

View File

@ -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(
() => ({

View File

@ -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

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

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

View File

@ -0,0 +1 @@
export { default, mapStateToPanes } from './Show.jsx';

View File

@ -0,0 +1 @@
"modern"

View File

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

View File

@ -1 +1 @@
export { default, panesMap } from './Show.jsx';
export { default, mapStateToPanes } from './Show.jsx';

View File

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

View File

@ -1 +1 @@
export { default, panesMap } from './Show.jsx';
export { default, mapStateToPanes } from './Show.jsx';

View File

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

View File

@ -1 +1 @@
export { default, panesMap } from './Show.jsx';
export { default, mapStateToPanes } from './Show.jsx';

View File

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

View File

@ -1 +1 @@
export { default, panesMap } from './Show.jsx';
export { default, mapStateToPanes } from './Show.jsx';

View File

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

View File

@ -1 +1 @@
export { default, panesMap } from './Show.jsx';
export { default, mapStateToPanes } from './Show.jsx';

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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": {}
}
]

View File

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