diff --git a/common/app/routes/challenges/Completion-Modal.jsx b/common/app/routes/challenges/Completion-Modal.jsx
new file mode 100644
index 0000000000..242a0cda56
--- /dev/null
+++ b/common/app/routes/challenges/Completion-Modal.jsx
@@ -0,0 +1,104 @@
+import noop from 'lodash/noop';
+import React, { PureComponent, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { Button, Modal } from 'react-bootstrap';
+import FontAwesome from 'react-fontawesome';
+
+import ns from './ns.json';
+import {
+ closeChallengeModal,
+ submitChallenge,
+
+ challengeModalSelector,
+ successMessageSelector
+} from './redux';
+
+const mapStateToProps = createSelector(
+ challengeModalSelector,
+ successMessageSelector,
+ (isOpen, message) => ({
+ isOpen,
+ message
+ })
+);
+
+const mapDispatchToProps = function(dispatch) {
+ const dispatchers = {
+ close: () => dispatch(closeChallengeModal()),
+ submitChallenge: (e) => {
+ if (
+ e.keyCode === 13 &&
+ (e.ctrlKey || e.meta)
+ ) {
+ dispatch(submitChallenge());
+ }
+ }
+ };
+ return () => dispatchers;
+};
+
+const propTypes = {
+ close: PropTypes.func.isRequired,
+ isOpen: PropTypes.bool,
+ message: PropTypes.string,
+ submitChallenge: PropTypes.func.isRequired
+};
+
+export class CompletionModal extends PureComponent {
+ render() {
+ const {
+ close,
+ isOpen,
+ submitChallenge,
+ message
+ } = this.props;
+ return (
+
+
+ { message }
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+CompletionModal.displayName = 'CompletionModal';
+CompletionModal.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(CompletionModal);
diff --git a/common/app/routes/challenges/Show.jsx b/common/app/routes/challenges/Show.jsx
index 7d955c8c2b..1e03366527 100644
--- a/common/app/routes/challenges/Show.jsx
+++ b/common/app/routes/challenges/Show.jsx
@@ -5,61 +5,52 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PureComponent from 'react-pure-render/component';
+import CompletionModal from './Completion-Modal.jsx';
import Classic from './views/classic';
import Step from './views/step';
import Project from './views/project';
-import Video from './views/video';
import BackEnd from './views/backend';
+import { challengeMetaSelector } from './redux';
import {
+ updateTitle,
+ updateCurrentChallenge,
fetchChallenge,
- fetchChallenges,
- replaceChallenge,
- resetUi
-} from './redux/actions';
-import { challengeSelector } from './redux/selectors';
-import { updateTitle } from '../../redux/actions';
-import { makeToast } from '../../toasts/redux/actions';
+
+ challengeSelector,
+ langSelector
+} from '../../redux';
+import { makeToast } from '../../Toasts/redux';
const views = {
backend: BackEnd,
classic: Classic,
project: Project,
simple: Project,
- step: Step,
- video: Video
+ step: Step
};
const mapDispatchToProps = {
fetchChallenge,
- fetchChallenges,
makeToast,
- replaceChallenge,
- resetUi,
+ updateCurrentChallenge,
updateTitle
};
const mapStateToProps = createSelector(
challengeSelector,
- state => state.challengesApp.challenge,
- state => state.challengesApp.superBlocks,
- state => state.app.lang,
+ challengeMetaSelector,
+ langSelector,
(
- {
- challenge: { isTranslated } = {},
- viewType,
- title
- },
- challenge,
- superBlocks = [],
+ { dashedName, isTranslated },
+ { viewType, title },
lang
) => ({
lang,
isTranslated,
title,
- challenge,
- viewType,
- areChallengesLoaded: superBlocks.length > 0
+ challenge: dashedName,
+ viewType
})
);
@@ -74,26 +65,23 @@ const fetchOptions = {
};
const link = 'http://forum.freecodecamp.com/t/' +
- 'guidelines-for-translating-free-code-camp' +
- '-to-any-language/19111';
+ 'guidelines-for-translating-free-code-camp' +
+ '-to-any-language/19111';
const propTypes = {
areChallengesLoaded: PropTypes.bool,
- fetchChallenges: PropTypes.func.isRequired,
isStep: PropTypes.bool,
isTranslated: PropTypes.bool,
lang: PropTypes.string.isRequired,
makeToast: PropTypes.func.isRequired,
params: PropTypes.object.isRequired,
- replaceChallenge: PropTypes.func.isRequired,
- resetUi: PropTypes.func.isRequired,
title: PropTypes.string,
+ updateCurrentChallenge: PropTypes.func.isRequired,
updateTitle: PropTypes.func.isRequired,
viewType: PropTypes.string
};
export class Show extends PureComponent {
-
componentWillMount() {
const { lang, isTranslated, makeToast } = this.props;
if (lang !== 'en' && !isTranslated) {
@@ -106,27 +94,19 @@ export class Show extends PureComponent {
}
componentDidMount() {
- if (!this.props.areChallengesLoaded) {
- this.props.fetchChallenges();
- }
if (this.props.title) {
this.props.updateTitle(this.props.title);
}
}
- componentWillUnmount() {
- this.props.resetUi();
- }
-
componentWillReceiveProps(nextProps) {
const { title } = nextProps;
- const { block, dashedName } = nextProps.params;
+ const { dashedName } = nextProps.params;
const { lang, isTranslated } = nextProps;
- const { resetUi, updateTitle, replaceChallenge, makeToast } = this.props;
+ const { updateTitle, updateCurrentChallenge, makeToast } = this.props;
if (this.props.params.dashedName !== dashedName) {
+ updateCurrentChallenge(dashedName);
updateTitle(title);
- resetUi();
- replaceChallenge({ dashedName, block });
if (lang !== 'en' && !isTranslated) {
makeToast({
message: 'We haven\'t translated this challenge yet.',
@@ -140,7 +120,12 @@ export class Show extends PureComponent {
render() {
const { viewType } = this.props;
const View = views[viewType] || Classic;
- return
;
+ return (
+
+
+
+
+ );
}
}
diff --git a/common/app/routes/challenges/challenges.less b/common/app/routes/challenges/challenges.less
index efda7469a8..b1816a5368 100644
--- a/common/app/routes/challenges/challenges.less
+++ b/common/app/routes/challenges/challenges.less
@@ -78,4 +78,86 @@
word-wrap: break-word;
}
+@keyframes skeletonShimmer{
+ 0% {
+ transform: translateX(-48px);
+ }
+ 100% {
+ transform: translateX(1000px);
+ }
+}
+
+.@{ns}-shimmer {
+ position: relative;
+ min-height: 18px;
+
+ .row {
+ height: 18px;
+
+ .col-xs-12 {
+ padding-right: 12px;
+ height: 17px;
+ }
+ }
+
+ .sprite-wrapper {
+ background-color: #333;
+ height: 17px;
+ width: 75%;
+ }
+
+ .sprite {
+ animation-name: skeletonShimmer;
+ animation-duration: 2.5s;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ animation-direction: normal;
+ background: white;
+ box-shadow: 0 0 3px 2px;
+ height: 17px;
+ width: 2px;
+ z-index: 5;
+ }
+}
+
+.@{ns}-success-modal {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 50vh;
+
+ .modal-title {
+ color: @gray-lighter;
+ }
+
+ .modal-header {
+ background-color: @brand-primary;
+ margin-bottom: 0;
+
+ .close {
+ color: #eee;
+ font-size: 4rem;
+ opacity: 0.6;
+ transition: all 300ms ease-out;
+ margin-top: 0;
+ padding-left: 0;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+
+ .modal-body {
+ padding: 35px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .fa {
+ margin-right: 0;
+ }
+ }
+}
+
&{ @import "./views/index.less"; }
diff --git a/common/app/routes/challenges/index.js b/common/app/routes/challenges/index.js
index c422581b24..77768a2197 100644
--- a/common/app/routes/challenges/index.js
+++ b/common/app/routes/challenges/index.js
@@ -1,8 +1,20 @@
import Show from './Show.jsx';
-import _Map from './views/map';
+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';
-export function challengesRoute() {
+export function createPanesMap() {
return {
+ ...backendPanesMap,
+ ...classicPanesMap,
+ ...stepPanesMap,
+ ...projectPanesMap
+ };
+}
+
+export default function challengesRoutes() {
+ return [{
path: 'challenges(/:dashedName)',
component: Show,
onEnter(nextState, replace) {
@@ -11,19 +23,8 @@ export function challengesRoute() {
replace('/map');
}
}
- };
-}
-
-export function modernChallengesRoute() {
- return {
+ }, {
path: 'challenges/:block/:dashedName',
component: Show
- };
-}
-
-export function mapRoute() {
- return {
- path: 'map',
- component: _Map
- };
+ }];
}
diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js
deleted file mode 100644
index ca411382b3..0000000000
--- a/common/app/routes/challenges/redux/actions.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import { createAction } from 'redux-actions';
-import { setContent } from '../../../../utils/polyvinyl';
-import { getMouse, loggerToStr } from '../utils';
-
-import types from './types';
-
-// step
-export const stepForward = createAction(types.stepForward);
-export const stepBackward = createAction(types.stepBackward);
-export const goToStep = createAction(
- types.goToStep,
- (step, isUnlocked) => ({ step, isUnlocked })
-);
-export const completeAction = createAction(types.completeAction);
-export const updateUnlockedSteps = createAction(types.updateUnlockedSteps);
-export const openLightBoxImage = createAction(types.openLightBoxImage);
-export const closeLightBoxImage = createAction(types.closeLightBoxImage);
-
-// challenges
-export const fetchChallenge = createAction(
- types.fetchChallenge,
- (dashedName, block) => ({ dashedName, block })
-);
-export const fetchChallengeCompleted = createAction(
- types.fetchChallengeCompleted,
- (_, challenge) => challenge,
- entities => ({ entities })
-);
-export const closeChallengeModal = createAction(types.closeChallengeModal);
-export const resetUi = createAction(types.resetUi);
-export const updateHint = createAction(types.updateHint);
-export const lockUntrustedCode = createAction(types.lockUntrustedCode);
-export const unlockUntrustedCode = createAction(
- types.unlockUntrustedCode,
- () => null
-);
-export const updateSuccessMessage = createAction(types.updateSuccessMessage);
-export const fetchChallenges = createAction(types.fetchChallenges);
-export const fetchChallengesCompleted = createAction(
- types.fetchChallengesCompleted,
- (_, superBlocks) => superBlocks,
- entities => ({ entities })
-);
-
-export const updateCurrentChallenge = createAction(
- types.updateCurrentChallenge
-);
-export const resetChallenge = createAction(types.resetChallenge);
-// replaceChallenge(dashedname) => Action
-export const replaceChallenge = createAction(types.replaceChallenge);
-
-// map
-export const updateFilter = createAction(
- types.updateFilter,
- e => e.target.value
-);
-
-export const initMap = createAction(types.initMap);
-export const toggleThisPanel = createAction(types.toggleThisPanel);
-export const collapseAll = createAction(types.collapseAll);
-export const expandAll = createAction(types.expandAll);
-
-export const clearFilter = createAction(types.clearFilter);
-
-// files
-export const updateFile = createAction(
- types.updateFile,
- (content, file) => setContent(content, file)
-);
-
-export const updateFiles = createAction(types.updateFiles);
-
-// rechallenge
-export const executeChallenge = createAction(
- types.executeChallenge,
- () => null
-);
-
-export const updateMain = createAction(types.updateMain);
-export const frameMain = createAction(types.frameMain);
-export const frameTests = createAction(types.frameTests);
-
-export const runTests = createAction(types.runTests);
-export const updateTests = createAction(types.updateTests);
-
-export const initOutput = createAction(types.initOutput, loggerToStr);
-export const updateOutput = createAction(types.updateOutput, loggerToStr);
-
-export const checkChallenge = createAction(types.checkChallenge);
-
-export const showProjectSubmit = createAction(types.showProjectSubmit);
-
-export const submitChallenge = createAction(types.submitChallenge);
-export const moveToNextChallenge = createAction(types.moveToNextChallenge);
-
-// code storage
-export const saveCode = createAction(types.saveCode);
-export const loadCode = createAction(types.loadCode);
-export const savedCodeFound = createAction(
- types.savedCodeFound,
- (files, challenge) => ({ files, challenge })
-);
-export const clearSavedCode = createAction(types.clearSavedCode);
-
-
-// video challenges
-export const toggleQuestionView = createAction(types.toggleQuestionView);
-export const grabQuestion = createAction(types.grabQuestion, e => {
- let { pageX, pageY, touches } = e;
- if (touches) {
- e.preventDefault();
- // these re-assigns the values of pageX, pageY from touches
- ({ pageX, pageY } = touches[0]);
- }
- const delta = [pageX, pageY];
- const mouse = [0, 0];
-
- return { delta, mouse };
-});
-
-export const releaseQuestion = createAction(types.releaseQuestion);
-export const moveQuestion = createAction(
- types.moveQuestion,
- ({ e, delta }) => getMouse(e, delta)
-);
-
-// answer({
-// e: Event,
-// answer: Boolean,
-// userAnswer: Boolean,
-// info: String,
-// threshold: Number
-// }) => Action
-export const answerQuestion = createAction(types.answerQuestion);
-
-export const startShake = createAction(types.startShake);
-export const endShake = createAction(types.primeNextQuestion);
-
-export const goToNextQuestion = createAction(types.goToNextQuestion);
-export const videoCompleted = createAction(types.videoCompleted);
-
-// bug
-export const openBugModal = createAction(types.openBugModal);
-export const closeBugModal = createAction(types.closeBugModal);
-export const openIssueSearch = createAction(types.openIssueSearch);
-export const createIssue = createAction(types.createIssue);
diff --git a/common/app/routes/challenges/redux/answer-saga.js b/common/app/routes/challenges/redux/answer-saga.js
deleted file mode 100644
index f5bb4aece6..0000000000
--- a/common/app/routes/challenges/redux/answer-saga.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { Observable } from 'rx';
-import types from './types';
-import { getMouse } from '../utils';
-
-import { submitChallenge, videoCompleted } from './actions';
-import { createErrorObservable } from '../../../redux/actions';
-import { makeToast } from '../../../toasts/redux/actions';
-import { challengeSelector } from './selectors';
-
-export default function answerSaga(action$, getState) {
- return action$
- .filter(action => action.type === types.answerQuestion)
- .flatMap(({
- payload: {
- e,
- answer,
- userAnswer,
- info,
- threshold
- }
- }) => {
- const state = getState();
- const {
- challenge: { tests }
- } = challengeSelector(state);
- const {
- challengesApp: {
- currentQuestion,
- delta = [ 0, 0 ]
- }
- } = state;
-
- let finalAnswer;
- // drag answer, compute response
- if (typeof userAnswer === 'undefined') {
- const [positionX] = getMouse(e, delta);
-
- // question released under threshold
- if (Math.abs(positionX) < threshold) {
- return Observable.just(null);
- }
-
- if (positionX >= threshold) {
- finalAnswer = true;
- }
-
- if (positionX <= -threshold) {
- finalAnswer = false;
- }
- } else {
- finalAnswer = userAnswer;
- }
-
- // incorrect question
- if (answer !== finalAnswer) {
- let infoAction;
- if (info) {
- infoAction = makeToast({
- message: info,
- timeout: 5000
- });
- }
-
- return Observable
- .just({ type: types.endShake })
- .delay(500)
- .startWith(infoAction, { type: types.startShake });
- }
-
- if (tests[currentQuestion]) {
- return Observable
- .just({ type: types.goToNextQuestion })
- .delay(300)
- .startWith({ type: types.primeNextQuestion });
- }
-
-
- return Observable.just(submitChallenge())
- .delay(300)
- // moves question to the appropriate side of the screen
- .startWith(videoCompleted(finalAnswer))
- // end with action so we know it is ok to transition
- .concat(Observable.just({ type: types.transitionHike }))
- .catch(createErrorObservable);
- });
-}
diff --git a/common/app/routes/challenges/redux/bug-saga.js b/common/app/routes/challenges/redux/bug-epic.js
similarity index 81%
rename from common/app/routes/challenges/redux/bug-saga.js
rename to common/app/routes/challenges/redux/bug-epic.js
index c7a3610200..f1d261c570 100644
--- a/common/app/routes/challenges/redux/bug-saga.js
+++ b/common/app/routes/challenges/redux/bug-epic.js
@@ -1,5 +1,12 @@
-import types from '../redux/types';
-import { closeBugModal } from '../redux/actions';
+import { ofType } from 'redux-epic';
+import {
+ types,
+ closeBugModal,
+
+ filesSelector
+} from '../redux';
+
+import { currentChallengeSelector } from '../../../redux';
function filesToMarkdown(files = {}) {
const moreThenOneFile = Object.keys(files).length > 1;
@@ -22,19 +29,12 @@ function filesToMarkdown(files = {}) {
}, '\n');
}
-export default function bugSaga(actions$, getState, { window }) {
- return actions$
- .filter(({ type }) => (
- type === types.openIssueSearch ||
- type === types.createIssue
- ))
+export default function bugEpic(actions, { getState }, { window }) {
+ return actions::ofType(types.openIssueSearch, types.createIssue)
.map(({ type }) => {
- const {
- challengesApp: {
- legacyKey: challengeName,
- files
- }
- } = getState();
+ const state = getState();
+ const files = filesSelector(state);
+ const challengeName = currentChallengeSelector(state);
const {
navigator: { userAgent },
location: { href }
diff --git a/common/app/routes/challenges/redux/next-challenge-saga.js b/common/app/routes/challenges/redux/challenge-epic.js
similarity index 58%
rename from common/app/routes/challenges/redux/next-challenge-saga.js
rename to common/app/routes/challenges/redux/challenge-epic.js
index 77fd920113..3f69fe0bce 100644
--- a/common/app/routes/challenges/redux/next-challenge-saga.js
+++ b/common/app/routes/challenges/redux/challenge-epic.js
@@ -1,22 +1,59 @@
+import debug from 'debug';
import { Observable } from 'rx';
+import { combineEpics, ofType } from 'redux-epic';
import { push } from 'react-router-redux';
-import types from './types';
-import { resetUi, updateCurrentChallenge } from './actions';
-import { createErrorObservable } from '../../../redux/actions';
-import { makeToast } from '../../../toasts/redux/actions';
+
+import {
+ types,
+
+ updateMain,
+ challengeUpdated
+} from './';
+import { getNS as entitiesSelector } from '../../../entities';
import {
getNextChallenge,
getFirstChallengeOfNextBlock,
getFirstChallengeOfNextSuperBlock
} from '../utils';
-import debug from 'debug';
+import {
+ types as app,
+
+ createErrorObservable,
+ updateCurrentChallenge,
+
+ currentChallengeSelector,
+ challengeSelector,
+ superBlocksSelector
+} from '../../../redux';
+import { makeToast } from '../../../Toasts/redux';
const isDev = debug.enabled('fcc:*');
-const { moveToNextChallenge } = types;
-export default function nextChallengeSaga(actions$, getState) {
- return actions$
- .filter(({ type }) => type === moveToNextChallenge)
+export function challengeUpdatedEpic(actions, { getState }) {
+ return actions::ofType(app.updateCurrentChallenge)
+ .flatMap(() => {
+ const challenge = challengeSelector(getState());
+ return Observable.of(
+ challengeUpdated(challenge),
+ push(`/challenges/${challenge.block}/${challenge.dashedName}`)
+ );
+ });
+}
+
+// used to reset users code on request
+export function resetChallengeEpic(actions, { getState }) {
+ return actions::ofType(types.resetChallenge)
+ .flatMap(() => {
+ const currentChallenge = currentChallengeSelector(getState());
+ return Observable.of(
+ updateCurrentChallenge(currentChallenge),
+ updateMain()
+ );
+ });
+}
+
+export function nextChallengeEpic(actions, { getState }) {
+ return actions::ofType(types.moveToNextChallenge)
.flatMap(() => {
let nextChallenge;
// let message = '';
@@ -24,8 +61,9 @@ export default function nextChallengeSaga(actions$, getState) {
// let isNewSuperBlock = false;
try {
const state = getState();
- const { challenge, superBlocks } = state.challengesApp;
- const { entities } = state;
+ const superBlocks = superBlocksSelector(state);
+ const challenge = currentChallengeSelector(state);
+ const entities = entitiesSelector(state);
nextChallenge = getNextChallenge(challenge, entities, { isDev });
// block completed.
if (!nextChallenge) {
@@ -73,13 +111,17 @@ export default function nextChallengeSaga(actions$, getState) {
);
}
return Observable.of(
- updateCurrentChallenge(nextChallenge),
- resetUi(),
- makeToast({ message: 'Your next challenge has arrived.' }),
- push(`/challenges/${nextChallenge.block}/${nextChallenge.dashedName}`)
+ updateCurrentChallenge(nextChallenge.dashedName),
+ makeToast({ message: 'Your next challenge has arrived.' })
);
} catch (err) {
return createErrorObservable(err);
}
});
}
+
+export default combineEpics(
+ challengeUpdatedEpic,
+ nextChallengeEpic,
+ resetChallengeEpic
+);
diff --git a/common/app/routes/challenges/redux/completion-saga.js b/common/app/routes/challenges/redux/completion-epic.js
similarity index 63%
rename from common/app/routes/challenges/redux/completion-saga.js
rename to common/app/routes/challenges/redux/completion-epic.js
index 9205cc1153..ab7265aab1 100644
--- a/common/app/routes/challenges/redux/completion-saga.js
+++ b/common/app/routes/challenges/redux/completion-epic.js
@@ -1,25 +1,35 @@
import { Observable } from 'rx';
+import { ofType } from 'redux-epic';
-import types from './types.js';
import {
- moveToNextChallenge,
- clearSavedCode
-} from './actions.js';
+ types,
+
+ moveToNextChallenge,
+ clearSavedCode,
+
+ challengeMetaSelector,
+ filesSelector,
+ testsSelector
+} from './';
-import { challengeSelector } from './selectors.js';
import {
createErrorObservable,
+
+ challengeSelector,
+ csrfSelector,
+ userSelector
+} from '../../../redux';
+import {
updateUserPoints,
updateUserChallenge
-} from '../../../redux/actions.js';
+} from '../../../entities';
import { backEndProject } from '../../../utils/challengeTypes.js';
-import { makeToast } from '../../../toasts/redux/actions.js';
+import { makeToast } from '../../../Toasts/redux';
import { postJSON$ } from '../../../../utils/ajax-stream.js';
-import { ofType } from '../../../../utils/get-actions-of-type.js';
function postChallenge(url, username, _csrf, challengeInfo) {
const body = { ...challengeInfo, _csrf };
- const saveChallenge$ = postJSON$(url, body)
+ const saveChallenge = postJSON$(url, body)
.retry(3)
.flatMap(({ points, lastUpdated, completedDate }) => {
return Observable.of(
@@ -32,29 +42,27 @@ function postChallenge(url, username, _csrf, challengeInfo) {
);
})
.catch(createErrorObservable);
- const challengeCompleted$ = Observable.of(moveToNextChallenge());
- return Observable.merge(saveChallenge$, challengeCompleted$);
+ const challengeCompleted = Observable.of(moveToNextChallenge());
+ return Observable.merge(saveChallenge, challengeCompleted);
}
function submitModern(type, state) {
- const { tests } = state.challengesApp;
+ const tests = testsSelector(state);
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
if (type === types.checkChallenge) {
- return Observable.just(null);
+ return Observable.empty();
}
if (type === types.submitChallenge) {
- const { challenge: { id } } = challengeSelector(state);
- const {
- app: { user, csrfToken },
- challengesApp: { files }
- } = state;
- const challengeInfo = { id, files };
+ const { id } = challengeSelector(state);
+ const files = filesSelector(state);
+ const { username } = userSelector(state);
+ const csrfToken = csrfSelector(state);
return postChallenge(
'/modern-challenge-completed',
- user,
+ username,
csrfToken,
- challengeInfo
+ { id, files }
);
}
}
@@ -64,42 +72,36 @@ function submitModern(type, state) {
}
function submitProject(type, state, { solution, githubLink }) {
- const {
- challenge: { id, challengeType }
- } = challengeSelector(state);
- const {
- app: { user, csrfToken }
- } = state;
+ const { id, challengeType } = challengeSelector(state);
+ const { username } = userSelector(state);
+ const csrfToken = csrfSelector(state);
const challengeInfo = { id, challengeType, solution };
if (challengeType === backEndProject) {
challengeInfo.githubLink = githubLink;
}
return postChallenge(
'/project-completed',
- user,
+ username,
csrfToken,
challengeInfo
);
}
function submitSimpleChallenge(type, state) {
- const {
- challenge: { id }
- } = challengeSelector(state);
- const {
- app: { user, csrfToken }
- } = state;
+ const { id } = challengeSelector(state);
+ const { username } = userSelector(state);
+ const csrfToken = csrfSelector(state);
const challengeInfo = { id };
return postChallenge(
'/challenge-completed',
- user,
+ username,
csrfToken,
challengeInfo
);
}
function submitBackendChallenge(type, state, { solution }) {
- const { tests } = state.challengesApp;
+ const tests = testsSelector(state);
if (
type === types.checkChallenge &&
tests.length > 0 &&
@@ -115,13 +117,13 @@ function submitBackendChallenge(type, state, { solution }) {
})
);
*/
-
- const { challenge: { id } } = challengeSelector(state);
- const { app: { user, csrfToken } } = state;
+ const { id } = challengeSelector(state);
+ const { username } = userSelector(state);
+ const csrfToken = csrfSelector(state);
const challengeInfo = { id, solution };
return postChallenge(
'/backend-challenge-completed',
- user,
+ username,
csrfToken,
challengeInfo
);
@@ -141,14 +143,12 @@ const submitters = {
'project.simple': submitSimpleChallenge
};
-export default function completionSaga(actions$, getState) {
- return actions$
- ::ofType(types.checkChallenge, types.submitChallenge)
+export default function completionEpic(actions, { getState }) {
+ return actions::ofType(types.checkChallenge, types.submitChallenge)
.flatMap(({ type, payload }) => {
const state = getState();
- const { submitType } = challengeSelector(state);
- const submitter = submitters[submitType] ||
- (() => Observable.just(null));
+ const { submitType } = challengeMetaSelector(state);
+ const submitter = submitters[submitType] || (() => Observable.empty());
return submitter(type, state, payload);
});
}
diff --git a/common/app/routes/challenges/redux/editor-epic.js b/common/app/routes/challenges/redux/editor-epic.js
new file mode 100644
index 0000000000..33eda718cc
--- /dev/null
+++ b/common/app/routes/challenges/redux/editor-epic.js
@@ -0,0 +1,17 @@
+import { ofType } from 'redux-epic';
+
+import {
+ types,
+ updateFile,
+
+ keySelector
+} from './';
+
+export default function editorEpic(actions, { getState }) {
+ return actions::ofType(types.classicEditorUpdated)
+ .pluck('payload')
+ .map(content => updateFile({
+ content,
+ key: keySelector(getState())
+ }));
+}
diff --git a/common/app/routes/challenges/redux/fetch-challenges-saga.js b/common/app/routes/challenges/redux/fetch-challenges-saga.js
deleted file mode 100644
index 5ad27638d3..0000000000
--- a/common/app/routes/challenges/redux/fetch-challenges-saga.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Observable } from 'rx';
-import debug from 'debug';
-
-import { challengeSelector } from './selectors';
-import types from './types';
-import {
- fetchChallengeCompleted,
- fetchChallengesCompleted,
- updateCurrentChallenge,
- initMap
-} from './actions';
-import {
- createMapUi,
- filterComingSoonBetaFromEntities,
- searchableChallengeTitles
-} from '../utils';
-import {
- delayedRedirect,
- createErrorObservable
-} from '../../../redux/actions';
-import createNameIdMap from '../../../../utils/create-name-id-map';
-
-const isDev = debug.enabled('fcc:*');
-
-const { fetchChallenge, fetchChallenges, replaceChallenge } = types;
-
-export default function fetchChallengesSaga(action$, getState, { services }) {
- return action$
- .filter(({ type }) => (
- type === fetchChallenges ||
- type === fetchChallenge ||
- type === replaceChallenge
- ))
- .flatMap(({ type, payload: { dashedName, block } = {} }) => {
- const state = getState();
- const lang = state.app.lang;
- if (type === replaceChallenge) {
- const { challenge: newChallenge } = challengeSelector({
- ...state,
- challengesApp: {
- ...state.challengesApp,
- challenge: dashedName
- }
- });
- if (state.challengesApp.challenge !== newChallenge.dashedName) {
- return Observable.just(updateCurrentChallenge(newChallenge));
- }
- return Observable.just(null);
- }
- const options = { service: 'map' };
- options.params = { lang };
- if (type === fetchChallenge) {
- options.params.dashedName = dashedName;
- options.params.block = block;
- }
- return services.readService$(options)
- .retry(3)
- .flatMap(({ entities, result, redirect } = {}) => {
- if (type === fetchChallenge) {
- return Observable.of(
- fetchChallengeCompleted(
- createNameIdMap(entities),
- result
- ),
- updateCurrentChallenge(entities.challenge[result.challenge]),
- redirect ? delayedRedirect(redirect) : null
- );
- }
- const filteredEntities = filterComingSoonBetaFromEntities(
- entities,
- isDev
- );
- const searchNames = searchableChallengeTitles(filteredEntities);
- return Observable.of(
- fetchChallengesCompleted(
- createNameIdMap(filteredEntities),
- result
- ),
- initMap(
- createMapUi(
- filteredEntities,
- result,
- searchNames
- )),
- );
- })
- .catch(createErrorObservable);
- });
-}
diff --git a/common/app/routes/challenges/redux/index.js b/common/app/routes/challenges/redux/index.js
index 1ea0b8dbea..61cef59898 100644
--- a/common/app/routes/challenges/redux/index.js
+++ b/common/app/routes/challenges/redux/index.js
@@ -1,25 +1,366 @@
-import fetchChallengesSaga from './fetch-challenges-saga';
-import completionSaga from './completion-saga';
-import nextChallengeSaga from './next-challenge-saga';
-import answerSaga from './answer-saga';
-import resetChallengeSaga from './reset-challenge-saga';
-import bugSaga from './bug-saga';
-import mapUiSaga from './map-ui-saga';
-import stepChallengeEpic from './step-challenge-epic';
+import { createTypes } from 'redux-create-types';
+import { createAction, combineActions, handleActions } from 'redux-actions';
+import { createSelector } from 'reselect';
+import noop from 'lodash/noop';
-export * as actions from './actions';
-export reducer from './reducer';
-export types from './types';
+import bugEpic from './bug-epic';
+import completionEpic from './completion-epic.js';
+import challengeEpic from './challenge-epic.js';
+import editorEpic from './editor-epic.js';
-export projectNormalizer from './project-normalizer';
+import ns from '../ns.json';
+import {
+ arrayToString,
+ buildSeed,
+ createTests,
+ getFileKey,
+ getPreFile,
+ loggerToStr,
+ submitTypes,
+ viewTypes
+} from '../utils';
+import {
+ types as app,
+ challengeSelector
+} from '../../../redux';
+import { bonfire, html, js } from '../../../utils/challengeTypes';
+import blockNameify from '../../../utils/blockNameify';
+import { createPoly, setContent } from '../../../../utils/polyvinyl';
+import createStepReducer, { epics as stepEpics } from '../views/step/redux';
+import createProjectReducer from '../views/project/redux';
-export const sagas = [
- fetchChallengesSaga,
- completionSaga,
- nextChallengeSaga,
- answerSaga,
- resetChallengeSaga,
- bugSaga,
- mapUiSaga,
- stepChallengeEpic
+// this is not great but is ok until we move to a different form type
+export projectNormalizer from '../views/project/redux';
+
+export const epics = [
+ bugEpic,
+ completionEpic,
+ challengeEpic,
+ editorEpic,
+ ...stepEpics
];
+
+export const types = createTypes([
+ // challenges
+ // |- classic
+ 'classicEditorUpdated',
+ 'challengeUpdated',
+ 'resetChallenge',
+ 'updateHint',
+ 'lockUntrustedCode',
+ 'unlockUntrustedCode',
+ 'closeChallengeModal',
+ 'updateSuccessMessage',
+
+ // files
+ 'updateFile',
+ 'updateFiles',
+
+ // rechallenge
+ 'executeChallenge',
+ 'updateMain',
+ 'runTests',
+ 'frameMain',
+ 'frameTests',
+ 'updateOutput',
+ 'initOutput',
+ 'updateTests',
+ 'checkChallenge',
+ 'submitChallenge',
+ 'moveToNextChallenge',
+
+ // code storage
+ 'saveCode',
+ 'loadCode',
+ 'savedCodeFound',
+ 'clearSavedCode',
+
+ // bug
+ 'openBugModal',
+ 'closeBugModal',
+ 'openIssueSearch',
+ 'createIssue',
+
+ // panes
+ 'toggleClassicEditor',
+ 'toggleMain',
+ 'toggleMap',
+ 'togglePreview',
+ 'toggleSidePanel',
+ 'toggleStep'
+], ns);
+
+// classic
+export const classicEditorUpdated = createAction(types.classicEditorUpdated);
+// challenges
+export const closeChallengeModal = createAction(types.closeChallengeModal);
+export const updateHint = createAction(types.updateHint);
+export const lockUntrustedCode = createAction(types.lockUntrustedCode);
+export const unlockUntrustedCode = createAction(
+ types.unlockUntrustedCode,
+ () => null
+);
+export const updateSuccessMessage = createAction(types.updateSuccessMessage);
+export const challengeUpdated = createAction(
+ types.challengeUpdated,
+ challenge => ({ challenge })
+);
+export const resetChallenge = createAction(types.resetChallenge);
+// files
+export const updateFile = createAction(types.updateFile);
+export const updateFiles = createAction(types.updateFiles);
+
+// rechallenge
+export const executeChallenge = createAction(
+ types.executeChallenge,
+ noop,
+);
+
+export const updateMain = createAction(types.updateMain);
+export const frameMain = createAction(types.frameMain);
+export const frameTests = createAction(types.frameTests);
+
+export const runTests = createAction(types.runTests);
+export const updateTests = createAction(types.updateTests);
+
+export const initOutput = createAction(types.initOutput, loggerToStr);
+export const updateOutput = createAction(types.updateOutput, loggerToStr);
+
+export const checkChallenge = createAction(types.checkChallenge);
+
+export const submitChallenge = createAction(types.submitChallenge);
+export const moveToNextChallenge = createAction(types.moveToNextChallenge);
+
+// code storage
+export const saveCode = createAction(types.saveCode);
+export const loadCode = createAction(types.loadCode);
+export const savedCodeFound = createAction(
+ types.savedCodeFound,
+ (files, challenge) => ({ files, challenge })
+);
+export const clearSavedCode = createAction(types.clearSavedCode);
+
+// bug
+export const openBugModal = createAction(types.openBugModal);
+export const closeBugModal = createAction(types.closeBugModal);
+export const openIssueSearch = createAction(types.openIssueSearch);
+export const createIssue = createAction(types.createIssue);
+
+const initialUiState = {
+ output: null,
+ isChallengeModalOpen: false,
+ isBugOpen: false,
+ successMessage: 'Happy Coding!',
+ hintIndex: 0,
+ numOfHints: 0
+};
+
+const initialState = {
+ isCodeLocked: false,
+ id: '',
+ challenge: '',
+ helpChatRoom: 'Help',
+ // old code storage key
+ legacyKey: '',
+ files: {},
+ // map
+ superBlocks: [],
+ // misc
+ ...initialUiState
+};
+
+export const getNS = state => state[ns];
+export const keySelector = state => getNS(state).key;
+export const filesSelector = state => getNS(state).files;
+export const testsSelector = state => getNS(state).tests;
+
+export const outputSelector = state => getNS(state).output;
+export const successMessageSelector = state => getNS(state).successMessage;
+export const hintIndexSelector = state => getNS(state).hintIndex;
+export const codeLockedSelector = state => getNS(state).isCodeLocked;
+export const chatRoomSelector = state => getNS(state).helpChatRoom;
+export const challengeModalSelector =
+ state => getNS(state).isChallengeModalOpen;
+
+export const bugModalSelector = state => getNS(state).isBugOpen;
+
+export const challengeMetaSelector = createSelector(
+ challengeSelector,
+ challenge => {
+ if (!challenge.id) {
+ return {};
+ }
+ const challengeType = challenge && challenge.challengeType;
+ const type = challenge && challenge.type;
+ const viewType = viewTypes[type] || viewTypes[challengeType] || 'classic';
+ const blockName = blockNameify(challenge.block);
+ const title = blockName && challenge.title ?
+ `${blockName}: ${challenge.title}` :
+ challenge.title;
+
+ return {
+ title,
+ viewType,
+ submitType:
+ submitTypes[challengeType] ||
+ submitTypes[challenge && challenge.type] ||
+ 'tests',
+ showPreview: challengeType === html,
+ mode: challenge && challengeType === html ?
+ 'text/html' :
+ 'javascript'
+ };
+ }
+);
+
+export default function createReducers() {
+ const setChallengeType = combineActions(
+ types.challengeUpdated,
+ app.fetchChallenge.complete
+ );
+
+ const mainReducer = handleActions(
+ {
+ [setChallengeType]: (state, { payload: { challenge } }) => {
+ return {
+ ...state,
+ ...initialUiState,
+ id: challenge.id,
+ challenge: challenge.dashedName,
+ key: getFileKey(challenge),
+ tests: createTests(challenge),
+ helpChatRoom: challenge.helpRoom || 'Help',
+ numOfHints: Array.isArray(challenge.hints) ?
+ challenge.hints.length :
+ 0
+ };
+ },
+ [types.updateTests]: (state, { payload: tests }) => ({
+ ...state,
+ tests,
+ isChallengeModalOpen: (
+ tests.length > 0 &&
+ tests.every(test => test.pass && !test.err)
+ )
+ }),
+ [types.closeChallengeModal]: state => ({
+ ...state,
+ isChallengeModalOpen: false
+ }),
+ [types.updateSuccessMessage]: (state, { payload }) => ({
+ ...state,
+ successMessage: payload
+ }),
+ [types.updateHint]: state => ({
+ ...state,
+ hintIndex: state.hintIndex + 1 >= state.numOfHints ?
+ 0 :
+ state.hintIndex + 1
+ }),
+ [types.lockUntrustedCode]: state => ({
+ ...state,
+ isCodeLocked: true
+ }),
+ [types.unlockUntrustedCode]: state => ({
+ ...state,
+ isCodeLocked: false
+ }),
+ [types.executeChallenge]: state => ({
+ ...state,
+ tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
+ }),
+
+ // classic/modern
+ [types.initOutput]: (state, { payload: output }) => ({
+ ...state,
+ output
+ }),
+ [types.updateOutput]: (state, { payload: output }) => ({
+ ...state,
+ output: (state.output || '') + output
+ }),
+
+ [types.openBugModal]: state => ({ ...state, isBugOpen: true }),
+ [types.closeBugModal]: state => ({ ...state, isBugOpen: false })
+ },
+ initialState
+ );
+
+ const filesReducer = handleActions(
+ {
+ [types.updateFile]: (state, { payload: { key, content }}) => ({
+ ...state,
+ [key]: setContent(content, state[key])
+ }),
+ [types.updateFiles]: (state, { payload: files }) => {
+ return files
+ .reduce((files, file) => {
+ files[file.key] = file;
+ return files;
+ }, { ...state });
+ },
+ [types.savedCodeFound]: (state, { payload: { files, challenge } }) => {
+ if (challenge.type === 'mod') {
+ // this may need to change to update head/tail
+ return challenge.files;
+ }
+ if (
+ challenge.challengeType !== html &&
+ challenge.challengeType !== js &&
+ challenge.challengeType !== bonfire
+ ) {
+ return {};
+ }
+ // classic challenge to modern format
+ const preFile = getPreFile(challenge);
+ return {
+ [preFile.key]: createPoly({
+ ...files[preFile.key],
+ // make sure head/tail are always fresh
+ head: arrayToString(challenge.head),
+ tail: arrayToString(challenge.tail)
+ })
+ };
+ },
+ [setChallengeType]: (state, { payload: { challenge } }) => {
+ if (challenge.type === 'mod') {
+ return challenge.files;
+ }
+ if (
+ challenge.challengeType !== html &&
+ challenge.challengeType !== js &&
+ challenge.challengeType !== bonfire
+ ) {
+ return {};
+ }
+ // classic challenge to modern format
+ const preFile = getPreFile(challenge);
+ return {
+ [preFile.key]: createPoly({
+ ...preFile,
+ contents: buildSeed(challenge),
+ head: arrayToString(challenge.head),
+ tail: arrayToString(challenge.tail)
+ })
+ };
+ }
+ },
+ {}
+ );
+
+ function reducer(state, action) {
+ const newState = mainReducer(state, action);
+ const files = filesReducer(state && state.files || {}, action);
+ if (newState.files !== files) {
+ return { ...newState, files };
+ }
+ return newState;
+ }
+
+ reducer.toString = () => ns;
+ return [
+ reducer,
+ ...createStepReducer(),
+ ...createProjectReducer()
+ ];
+}
diff --git a/common/app/routes/challenges/redux/map-ui-saga.js b/common/app/routes/challenges/redux/map-ui-saga.js
deleted file mode 100644
index 9d3cff1c61..0000000000
--- a/common/app/routes/challenges/redux/map-ui-saga.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import types from './types';
-import { initMap } from './actions';
-import { unfilterMapUi, applyFilterToMap } from '../utils';
-
-export default function mapUiSaga(actions$, getState) {
- return actions$
- .filter(({ type }) => (
- type === types.updateFilter ||
- type === types.clearFilter
- ))
- .debounce(250)
- .map(({ payload: filter = '' }) => filter)
- .distinctUntilChanged()
- .map(filter => {
- const { challengesApp: { mapUi = {} } } = getState();
- let newMapUi;
- if (filter.length <= 3) {
- newMapUi = unfilterMapUi(mapUi);
- } else {
- const regexString = filter
- // replace spaces with any key to match dashes
- .replace(/ /g, '.')
- // makes search more fuzzy (thanks @xRahul)
- .split('')
- .join('.*');
- const filterRegex = new RegExp(regexString, 'i');
- newMapUi = applyFilterToMap(mapUi, filterRegex);
- }
- if (!newMapUi || newMapUi === mapUi) {
- return null;
- }
- return initMap(newMapUi);
- });
-}
diff --git a/common/app/routes/challenges/redux/reducer.js b/common/app/routes/challenges/redux/reducer.js
deleted file mode 100644
index cda99f4fb2..0000000000
--- a/common/app/routes/challenges/redux/reducer.js
+++ /dev/null
@@ -1,333 +0,0 @@
-import { handleActions } from 'redux-actions';
-import { createPoly } from '../../../../utils/polyvinyl';
-
-import types from './types';
-import { bonfire, html, js } from '../../../utils/challengeTypes';
-import {
- arrayToString,
- buildSeed,
- createTests,
- getPreFile,
- getFileKey,
- toggleThisPanel,
- collapseAllPanels,
- expandAllPanels
-} from '../utils';
-
-const initialUiState = {
- hintIndex: 0,
- // step index tracing
- currentIndex: 0,
- previousIndex: -1,
- // step action
- isActionCompleted: false,
- isLightBoxOpen: false,
- // project is ready to submit
- isSubmitting: false,
- output: null,
- // video
- // 1 indexed
- currentQuestion: 1,
- // [ xPosition, yPosition ]
- mouse: [ 0, 0 ],
- // change in mouse position since pressed
- // [ xDelta, yDelta ]
- delta: [ 0, 0 ],
- isPressed: false,
- isCorrect: false,
- shouldShakeQuestion: false,
- shouldShowQuestions: false,
- isChallengeModalOpen: false,
- successMessage: 'Happy Coding!',
- unlockedSteps: []
-};
-const initialState = {
- isCodeLocked: false,
- id: '',
- challenge: '',
- helpChatRoom: 'Help',
- isBugOpen: false,
- // old code storage key
- legacyKey: '',
- files: {},
- // map
- mapUi: { isAllCollapsed: false },
- filter: '',
- superBlocks: [],
- // misc
- toast: 0,
- ...initialUiState
-};
-
-const mainReducer = handleActions(
- {
- [types.fetchChallengeCompleted]: (state, { payload = '' }) => ({
- ...state,
- challenge: payload
- }),
- [types.updateCurrentChallenge]: (state, { payload: challenge = {} }) => ({
- ...state,
- id: challenge.id,
- // used mainly to find code storage
- legacyKey: challenge.name,
- challenge: challenge.dashedName,
- key: getFileKey(challenge),
- tests: createTests(challenge),
- helpChatRoom: challenge.helpRoom || 'Help',
- numOfHints: Array.isArray(challenge.hints) ? challenge.hints.length : 0
- }),
- [types.updateTests]: (state, { payload: tests }) => ({
- ...state,
- tests,
- isChallengeModalOpen: (
- tests.length > 0 &&
- tests.every(test => test.pass && !test.err)
- )
- }),
- [types.closeChallengeModal]: state => ({
- ...state,
- isChallengeModalOpen: false
- }),
- [types.updateSuccessMessage]: (state, { payload }) => ({
- ...state,
- successMessage: payload
- }),
- [types.updateHint]: state => ({
- ...state,
- hintIndex: state.hintIndex + 1 >= state.numOfHints ?
- 0 :
- state.hintIndex + 1
- }),
- [types.lockUntrustedCode]: state => ({
- ...state,
- isCodeLocked: true
- }),
- [types.unlockUntrustedCode]: state => ({
- ...state,
- isCodeLocked: false
- }),
- [types.executeChallenge]: state => ({
- ...state,
- tests: state.tests.map(test => ({ ...test, err: false, pass: false }))
- }),
- [types.showChallengeComplete]: (state, { payload: toast }) => ({
- ...state,
- toast
- }),
- [types.showProjectSubmit]: state => ({
- ...state,
- isSubmitting: true
- }),
- [types.resetUi]: (state) => ({
- ...state,
- ...initialUiState
- }),
-
- // map
- [types.updateFilter]: (state, { payload = ''}) => ({
- ...state,
- filter: payload
- }),
- [types.clearFilter]: (state) => ({
- ...state,
- filter: ''
- }),
- [types.fetchChallengesCompleted]: (state, { payload = [] }) => ({
- ...state,
- superBlocks: payload
- }),
-
- // step
- [types.goToStep]: (state, { payload: { step = 0, isUnlocked }}) => ({
- ...state,
- currentIndex: step,
- previousIndex: state.currentIndex,
- isActionCompleted: isUnlocked
- }),
- [types.completeAction]: state => ({
- ...state,
- isActionCompleted: true
- }),
- [types.updateUnlockedSteps]: (state, { payload }) => ({
- ...state,
- unlockedSteps: payload
- }),
- [types.openLightBoxImage]: state => ({
- ...state,
- isLightBoxOpen: true
- }),
- [types.closeLightBoxImage]: state => ({
- ...state,
- isLightBoxOpen: false
- }),
-
- // classic/modern
- [types.initOutput]: (state, { payload: output }) => ({
- ...state,
- output
- }),
- [types.updateOutput]: (state, { payload: output }) => ({
- ...state,
- output: (state.output || '') + output
- }),
- // video
- [types.toggleQuestionView]: state => ({
- ...state,
- shouldShowQuestions: !state.shouldShowQuestions,
- currentQuestion: 1
- }),
-
- [types.grabQuestion]: (state, { payload: { delta, mouse } }) => ({
- ...state,
- isPressed: true,
- delta,
- mouse
- }),
-
- [types.releaseQuestion]: state => ({
- ...state,
- isPressed: false,
- mouse: [ 0, 0 ]
- }),
-
- [types.moveQuestion]: (state, { payload: mouse }) => ({ ...state, mouse }),
- [types.startShake]: state => ({ ...state, shouldShakeQuestion: true }),
- [types.endShake]: state => ({ ...state, shouldShakeQuestion: false }),
-
- [types.primeNextQuestion]: (state, { payload: userAnswer }) => ({
- ...state,
- currentQuestion: state.currentQuestion + 1,
- mouse: [ userAnswer ? 1000 : -1000, 0],
- isPressed: false
- }),
-
- [types.goToNextQuestion]: state => ({
- ...state,
- mouse: [ 0, 0 ]
- }),
-
- [types.videoCompleted]: (state, { payload: userAnswer }) => ({
- ...state,
- isCorrect: true,
- isPressed: false,
- delta: [ 0, 0 ],
- mouse: [ userAnswer ? 1000 : -1000, 0]
- }),
-
- [types.openBugModal]: state => ({ ...state, isBugOpen: true }),
- [types.closeBugModal]: state => ({ ...state, isBugOpen: false })
- },
- initialState
-);
-
-const filesReducer = handleActions(
- {
- [types.updateFile]: (state, { payload: file }) => ({
- ...state,
- [file.key]: file
- }),
- [types.updateFiles]: (state, { payload: files }) => {
- return files
- .reduce((files, file) => {
- files[file.key] = file;
- return files;
- }, { ...state });
- },
- [types.savedCodeFound]: (state, { payload: { files, challenge } }) => {
- if (challenge.type === 'mod') {
- // this may need to change to update head/tail
- return challenge.files;
- }
- if (
- challenge.challengeType !== html &&
- challenge.challengeType !== js &&
- challenge.challengeType !== bonfire
- ) {
- return {};
- }
- // classic challenge to modern format
- const preFile = getPreFile(challenge);
- return {
- [preFile.key]: createPoly({
- ...files[preFile.key],
- // make sure head/tail are always fresh
- head: arrayToString(challenge.head),
- tail: arrayToString(challenge.tail)
- })
- };
- },
- [types.updateCurrentChallenge]: (state, { payload: challenge = {} }) => {
- if (challenge.type === 'mod') {
- return challenge.files;
- }
- if (
- challenge.challengeType !== html &&
- challenge.challengeType !== js &&
- challenge.challengeType !== bonfire
- ) {
- return {};
- }
- // classic challenge to modern format
- const preFile = getPreFile(challenge);
- return {
- [preFile.key]: createPoly({
- ...preFile,
- contents: buildSeed(challenge),
- head: arrayToString(challenge.head),
- tail: arrayToString(challenge.tail)
- })
- };
- }
- },
- {}
-);
-
-// {
-// children: [...{
-// name: (superBlock: String),
-// isOpen: Boolean,
-// isHidden: Boolean,
-// children: [...{
-// name: (blockName: String),
-// isOpen: Boolean,
-// isHidden: Boolean,
-// children: [...{
-// name: (challengeName: String),
-// isHidden: Boolean
-// }]
-// }]
-// }
-// }
-const mapReducer = handleActions(
- {
- [types.initMap]: (state, { payload }) => payload,
- [types.toggleThisPanel]: (state, { payload: name }) => {
- return toggleThisPanel(state, name);
- },
- [types.collapseAll]: state => {
- const newState = collapseAllPanels(state);
- newState.isAllCollapsed = true;
- return newState;
- },
- [types.expandAll]: state => {
- const newState = expandAllPanels(state);
- newState.isAllCollapsed = false;
- return newState;
- }
- },
- initialState.mapUi
-);
-
-export default function challengeReducers(state, action) {
- const newState = mainReducer(state, action);
- const files = filesReducer(state && state.files || {}, action);
- if (newState.files !== files) {
- return { ...newState, files };
- }
- // map actions only effect this reducer;
- const mapUi = mapReducer(state && state.mapUi || {}, action);
- if (newState.mapUi !== mapUi) {
- return { ...newState, mapUi };
- }
- return newState;
-}
diff --git a/common/app/routes/challenges/redux/reset-challenge-saga.js b/common/app/routes/challenges/redux/reset-challenge-saga.js
deleted file mode 100644
index 65ff5ec5ad..0000000000
--- a/common/app/routes/challenges/redux/reset-challenge-saga.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Observable } from 'rx';
-import types from './types';
-import {
- updateCurrentChallenge,
- updateMain
-} from './actions';
-
-export default function resetChallengeSaga(actions$, getState) {
- return actions$
- .filter(({ type }) => type === types.resetChallenge)
- .flatMap(() => {
- const {
- challengesApp: { challenge: dashedName },
- entities: { challenge: challengeMap }
- } = getState();
- const currentChallenge = challengeMap[dashedName];
- return Observable.of(
- updateCurrentChallenge(currentChallenge),
- updateMain()
- );
- });
-}
diff --git a/common/app/routes/challenges/redux/selectors.js b/common/app/routes/challenges/redux/selectors.js
deleted file mode 100644
index 7ab4063529..0000000000
--- a/common/app/routes/challenges/redux/selectors.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { createSelector } from 'reselect';
-
-import { viewTypes, submitTypes, getNode } from '../utils';
-import blockNameify from '../../../utils/blockNameify';
-import { html } from '../../../utils/challengeTypes';
-
-export const challengeSelector = createSelector(
- state => state.challengesApp.challenge,
- state => state.entities.challenge,
- (challengeName, challengeMap) => {
- if (!challengeName || !challengeMap) {
- return {};
- }
- const challenge = challengeMap[challengeName];
- const challengeType = challenge && challenge.challengeType;
- const type = challenge && challenge.type;
- const viewType = viewTypes[type] || viewTypes[challengeType] || 'classic';
- const blockName = blockNameify(challenge.block);
- const title = blockName && challenge.title ?
- `${blockName}: ${challenge.title}` :
- challenge.title;
- return {
- challenge,
- title,
- viewType,
- submitType:
- submitTypes[challengeType] ||
- submitTypes[challenge && challenge.type] ||
- 'tests',
- showPreview: challengeType === html,
- mode: challenge && challengeType === html ?
- 'text/html' :
- 'javascript'
- };
- }
-);
-
-export const makePanelOpenSelector = () => createSelector(
- state => state.challengesApp.mapUi,
- (_, props) => props.dashedName,
- (mapUi, name) => {
- const node = getNode(mapUi, name);
- return node ? node.isOpen : true;
- }
-);
-
-export const makePanelHiddenSelector = () => createSelector(
- state => state.challengesApp.mapUi,
- (_, props) => props.dashedName,
- (mapUi, name) => {
- const node = getNode(mapUi, name);
- return node ? node.isHidden : false;
- }
-);
diff --git a/common/app/routes/challenges/redux/types.js b/common/app/routes/challenges/redux/types.js
deleted file mode 100644
index d04cd0bb38..0000000000
--- a/common/app/routes/challenges/redux/types.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import createTypes from '../../../utils/create-types';
-
-export default createTypes([
- // step
- 'stepForward',
- 'stepBackward',
- 'goToStep',
- 'completeAction',
- 'openLightBoxImage',
- 'closeLightBoxImage',
- 'updateUnlockedSteps',
-
- // challenges
- 'fetchChallenge',
- 'fetchChallenges',
- 'fetchChallengeCompleted',
- 'fetchChallengesCompleted',
- 'updateCurrentChallenge',
- 'resetChallenge',
- 'replaceChallenge',
- 'resetUi',
- 'updateHint',
- 'lockUntrustedCode',
- 'unlockUntrustedCode',
- 'closeChallengeModal',
- 'updateSuccessMessage',
-
- // map
- 'updateFilter',
- 'clearFilter',
- 'initMap',
- 'toggleThisPanel',
- 'collapseAll',
- 'expandAll',
-
- // files
- 'updateFile',
- 'updateFiles',
-
- // rechallenge
- 'executeChallenge',
- 'updateMain',
- 'runTests',
- 'frameMain',
- 'frameTests',
- 'updateOutput',
- 'initOutput',
- 'updateTests',
- 'checkChallenge',
- 'showChallengeComplete',
- 'showProjectSubmit',
- 'submitChallenge',
- 'moveToNextChallenge',
-
- // code storage
- 'saveCode',
- 'loadCode',
- 'savedCodeFound',
- 'clearSavedCode',
-
- // video challenges
- 'toggleQuestionView',
- 'grabQuestion',
- 'releaseQuestion',
- 'moveQuestion',
-
- 'answerQuestion',
-
- 'startShake',
- 'endShake',
-
- 'primeNextQuestion',
- 'goToNextQuestion',
- 'transitionVideo',
- 'videoCompleted',
-
- // bug
- 'openBugModal',
- 'closeBugModal',
- 'openIssueSearch',
- 'createIssue'
-], 'challenges');
diff --git a/common/app/routes/challenges/utils.js b/common/app/routes/challenges/utils.js
index 29e65f2e36..39140c3429 100644
--- a/common/app/routes/challenges/utils.js
+++ b/common/app/routes/challenges/utils.js
@@ -1,6 +1,5 @@
import flow from 'lodash/flow';
import * as challengeTypes from '../../utils/challengeTypes';
-import protect from '../../utils/empty-protector';
import { decodeScriptTags } from '../../../utils/encode-decode';
// determine the component to view for each challenge
@@ -298,265 +297,3 @@ export function getCurrentSuperBlockName(current, entities) {
const block = blockMap[challenge.block];
return block.superBlock;
}
-
-// gets new mouse position
-// getMouse(
-// e: MouseEvent|TouchEvent,
-// [ dx: Number, dy: Number ]
-// ) => [ Number, Number ]
-export function getMouse(e, [dx, dy]) {
- let { pageX, pageY, touches, changedTouches } = e;
-
- // touches can be empty on touchend
- if (touches || changedTouches) {
- e.preventDefault();
- // these re-assigns the values of pageX, pageY from touches
- ({ pageX, pageY } = touches[0] || changedTouches[0]);
- }
-
- return [pageX - dx, pageY - dy];
-}
-
-export function filterComingSoonBetaChallenge(
- isDev = false,
- { isComingSoon, isBeta }
-) {
- return !(isComingSoon || isBeta) ||
- isDev;
-}
-
-export function filterComingSoonBetaFromEntities(
- { challenge: challengeMap, ...rest },
- isDev = false
-) {
- const filter = filterComingSoonBetaChallenge.bind(null, isDev);
- return {
- ...rest,
- challenge: Object.keys(challengeMap)
- .map(dashedName => challengeMap[dashedName])
- .filter(filter)
- .reduce((challengeMap, challenge) => {
- challengeMap[challenge.dashedName] = challenge;
- return challengeMap;
- }, {})
- };
-}
-
-export function searchableChallengeTitles({ challenge: challengeMap } = {}) {
- return Object.keys(challengeMap)
- .map(dashedName => challengeMap[dashedName])
- .reduce((accu, current) => {
- accu[current.dashedName] = current.title;
- return accu;
- }
- , {});
-}
-
-// interface Node {
-// isHidden: Boolean,
-// children: Void|[ ...Node ],
-// isOpen?: Boolean
-// }
-//
-// interface MapUi
-// {
-// children: [...{
-// name: (superBlock: String),
-// isOpen: Boolean,
-// isHidden: Boolean,
-// children: [...{
-// name: (blockName: String),
-// isOpen: Boolean,
-// isHidden: Boolean,
-// children: [...{
-// name: (challengeName: String),
-// isHidden: Boolean
-// }]
-// }]
-// }]
-// }
-export function createMapUi(
- { superBlock: superBlockMap, block: blockMap } = {},
- superBlocks,
- searchNameMap
-) {
- if (!superBlocks || !superBlockMap || !blockMap) {
- return {};
- }
- return {
- children: superBlocks.map(superBlock => {
- return {
- name: superBlock,
- isOpen: false,
- isHidden: false,
- children: protect(superBlockMap[superBlock]).blocks.map(block => {
- return {
- name: block,
- isOpen: false,
- isHidden: false,
- children: protect(blockMap[block]).challenges.map(challenge => {
- return {
- name: challenge,
- title: searchNameMap[challenge],
- isHidden: false,
- children: null
- };
- })
- };
- })
- };
- })
- };
-}
-
-// synchronise
-// traverseMapUi(
-// tree: MapUi|Node,
-// update: ((MapUi|Node) => MapUi|Node)
-// ) => MapUi|Node
-export function traverseMapUi(tree, update) {
- let childrenChanged;
- if (!Array.isArray(tree.children)) {
- return update(tree);
- }
- const newChildren = tree.children.map(node => {
- const newNode = traverseMapUi(node, update);
- if (!childrenChanged && newNode !== node) {
- childrenChanged = true;
- }
- return newNode;
- });
- if (childrenChanged) {
- tree = {
- ...tree,
- children: newChildren
- };
- }
- return update(tree);
-}
-
-// synchronise
-// getNode(tree: MapUi, name: String) => MapUi
-export function getNode(tree, name) {
- let node;
- traverseMapUi(tree, thisNode => {
- if (thisNode.name === name) {
- node = thisNode;
- }
- return thisNode;
- });
- return node;
-}
-
-// synchronise
-// updateSingelNode(
-// tree: MapUi,
-// name: String,
-// update(MapUi|Node) => MapUi|Node
-// ) => MapUi
-export function updateSingleNode(tree, name, update) {
- return traverseMapUi(tree, node => {
- if (name !== node.name) {
- return node;
- }
- return update(node);
- });
-}
-
-// synchronise
-// toggleThisPanel(tree: MapUi, name: String) => MapUi
-export function toggleThisPanel(tree, name) {
- return updateSingleNode(tree, name, node => {
- return {
- ...node,
- isOpen: !node.isOpen
- };
- });
-}
-
-// toggleAllPanels(tree: MapUi, isOpen: Boolean = false ) => MapUi
-export function toggleAllPanels(tree, isOpen = false) {
- return traverseMapUi(tree, node => {
- if (!Array.isArray(node.children) || node.isOpen === isOpen) {
- return node;
- }
- return {
- ...node,
- isOpen
- };
- });
-}
-
-// collapseAllPanels(tree: MapUi) => MapUi
-export function collapseAllPanels(tree) {
- return toggleAllPanels(tree);
-}
-
-// expandAllPanels(tree: MapUi) => MapUi
-export function expandAllPanels(tree) {
- return toggleAllPanels(tree, true);
-}
-
-// applyFilterToMap(tree: MapUi, filterRegex: RegExp) => MapUi
-export function applyFilterToMap(tree, filterRegex) {
- return traverseMapUi(
- tree,
- node => {
- // no children indicates a challenge node
- // if leaf (challenge) then test if regex is a match
- if (!Array.isArray(node.children)) {
- // does challenge name meet filter criteria?
- if (filterRegex.test(node.title)) {
- // is challenge currently hidden?
- if (node.isHidden) {
- // unhide challenge, it matches
- return {
- ...node,
- isHidden: false
- };
- }
- } else if (!node.isHidden) {
- return {
- ...node,
- isHidden: true
- };
- }
- return node;
- }
- // if not leaf node (challenge) then
- // test to see if all its children are hidden
- if (node.children.every(node => node.isHidden)) {
- if (node.isHidden) {
- return node;
- }
- return {
- ...node,
- isHidden: true
- };
- } else if (node.isHidden) {
- return {
- ...node,
- isHidden: false
- };
- }
- // nothing has changed
- return node;
- }
- );
-}
-
-// unfilterMapUi(tree: MapUi) => MapUi
-export function unfilterMapUi(tree) {
- return traverseMapUi(
- tree,
- node => {
- if (!node.isHidden) {
- return node;
- }
- return {
- ...node,
- isHidden: false
- };
- }
- );
-}
diff --git a/common/app/routes/challenges/utils.test.js b/common/app/routes/challenges/utils.test.js
index a93cf6845d..06956afcea 100644
--- a/common/app/routes/challenges/utils.test.js
+++ b/common/app/routes/challenges/utils.test.js
@@ -1,1263 +1,848 @@
import test from 'tape';
-import sinon from 'sinon';
import {
getNextChallenge,
getFirstChallengeOfNextBlock,
- getFirstChallengeOfNextSuperBlock,
- filterComingSoonBetaChallenge,
- filterComingSoonBetaFromEntities,
- createMapUi,
- traverseMapUi,
- getNode,
- updateSingleNode,
- toggleThisPanel,
- expandAllPanels,
- collapseAllPanels,
- applyFilterToMap,
- unfilterMapUi
+ getFirstChallengeOfNextSuperBlock
} from './utils.js';
+test('getNextChallenge', t => {
+ t.plan(7);
+ t.test('should return falsey when current challenge is not found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: {},
+ block: {}
+ };
+ t.notOk(
+ getNextChallenge('non-existent-challenge', entities),
+ 'getNextChallenge did not return falsey when challenge is not found'
+ );
+ });
+ t.test('should return falsey when last challenge in block', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const nextChallenge = {
+ dashedName: 'next-challenge',
+ block: 'current-block'
+ };
+ const shouldBeNext = getNextChallenge(
+ 'next-challenge',
+ {
+ challenge: {
+ 'current-challenge': currentChallenge,
+ 'next-challenge': nextChallenge
+ },
+ block: {
+ 'current-block': {
+ challenges: [
+ 'current-challenge',
+ 'next-challenge'
+ ]
+ }
+ }
+ }
+ );
+ t.false(
+ shouldBeNext,
+ 'getNextChallenge should return null or undefined'
+ );
+ });
-test('common/app/routes/challenges/utils', function(t) {
- t.test('getNextChallenge', t => {
- t.plan(7);
- t.test('should return falsey when current challenge is not found', t => {
- t.plan(1);
- const entities = {
- challenge: {},
- block: {}
- };
- t.notOk(
- getNextChallenge('non-existent-challenge', entities),
- 'getNextChallenge did not return falsey when challenge is not found'
- );
- });
- t.test('should return falsey when last challenge in block', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const nextChallenge = {
- dashedName: 'next-challenge',
- block: 'current-block'
- };
- const shouldBeNext = getNextChallenge(
- 'next-challenge',
- {
- challenge: {
- 'current-challenge': currentChallenge,
- 'next-challenge': nextChallenge
- },
- block: {
- 'current-block': {
- challenges: [
- 'current-challenge',
- 'next-challenge'
- ]
- }
+ t.test('should return next challenge when it exists', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const nextChallenge = {
+ dashedName: 'next-challenge',
+ block: 'current-block'
+ };
+ const shouldBeNext = getNextChallenge(
+ 'current-challenge',
+ {
+ challenge: {
+ 'current-challenge': currentChallenge,
+ 'next-challenge': nextChallenge
+ },
+ block: {
+ 'current-block': {
+ challenges: [
+ 'current-challenge',
+ 'next-challenge'
+ ]
}
}
- );
- t.false(
- shouldBeNext,
- 'getNextChallenge should return null or undefined'
- );
- });
-
- t.test('should return next challenge when it exists', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const nextChallenge = {
- dashedName: 'next-challenge',
- block: 'current-block'
- };
- const shouldBeNext = getNextChallenge(
- 'current-challenge',
- {
- challenge: {
- 'current-challenge': currentChallenge,
- 'next-challenge': nextChallenge
- },
- block: {
- 'current-block': {
- challenges: [
- 'current-challenge',
- 'next-challenge'
- ]
- }
- }
- }
- );
- t.isEqual(shouldBeNext, nextChallenge);
- });
- t.test('should skip isComingSoon challenge', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const comingSoon = {
- dashedName: 'coming-soon',
- isComingSoon: true,
- block: 'current-block'
- };
- const nextChallenge = {
- dashedName: 'next-challenge',
- block: 'current-block'
- };
- const shouldBeNext = getNextChallenge(
- 'current-challenge',
- {
- challenge: {
- 'current-challenge': currentChallenge,
- 'next-challenge': nextChallenge,
- 'coming-soon': comingSoon,
- 'coming-soon2': comingSoon
- },
- block: {
- 'current-block': {
- challenges: [
- 'current-challenge',
- 'coming-soon',
- 'coming-soon2',
- 'next-challenge'
- ]
- }
- }
- }
- );
- t.isEqual(shouldBeNext, nextChallenge);
- });
- t.test('should not skip isComingSoon challenge in dev', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const comingSoon = {
- dashedName: 'coming-soon',
- isComingSoon: true,
- block: 'current-block'
- };
- const nextChallenge = {
- dashedName: 'next-challenge',
- block: 'current-block'
- };
- const entities = {
+ }
+ );
+ t.isEqual(shouldBeNext, nextChallenge);
+ });
+ t.test('should skip isComingSoon challenge', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const comingSoon = {
+ dashedName: 'coming-soon',
+ isComingSoon: true,
+ block: 'current-block'
+ };
+ const nextChallenge = {
+ dashedName: 'next-challenge',
+ block: 'current-block'
+ };
+ const shouldBeNext = getNextChallenge(
+ 'current-challenge',
+ {
challenge: {
'current-challenge': currentChallenge,
'next-challenge': nextChallenge,
- 'coming-soon': comingSoon
+ 'coming-soon': comingSoon,
+ 'coming-soon2': comingSoon
},
block: {
'current-block': {
challenges: [
'current-challenge',
'coming-soon',
+ 'coming-soon2',
'next-challenge'
]
}
}
- };
- t.isEqual(
- getNextChallenge('current-challenge', entities, { isDev: true }),
- comingSoon
- );
- });
- t.test('should skip isBeta challenge', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const beta = {
- dashedName: 'beta-challenge',
- isBeta: true,
- block: 'current-block'
- };
- const nextChallenge = {
- dashedName: 'next-challenge',
- block: 'current-block'
- };
- const shouldBeNext = getNextChallenge(
- 'current-challenge',
- {
- challenge: {
- 'current-challenge': currentChallenge,
- 'next-challenge': nextChallenge,
- 'beta-challenge': beta,
- 'beta-challenge2': beta
- },
- block: {
- 'current-block': {
- challenges: [
- 'current-challenge',
- 'beta-challenge',
- 'beta-challenge2',
- 'next-challenge'
- ]
- }
- }
+ }
+ );
+ t.isEqual(shouldBeNext, nextChallenge);
+ });
+ t.test('should not skip isComingSoon challenge in dev', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const comingSoon = {
+ dashedName: 'coming-soon',
+ isComingSoon: true,
+ block: 'current-block'
+ };
+ const nextChallenge = {
+ dashedName: 'next-challenge',
+ block: 'current-block'
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': currentChallenge,
+ 'next-challenge': nextChallenge,
+ 'coming-soon': comingSoon
+ },
+ block: {
+ 'current-block': {
+ challenges: [
+ 'current-challenge',
+ 'coming-soon',
+ 'next-challenge'
+ ]
}
- );
- t.isEqual(shouldBeNext, nextChallenge);
- });
- t.test('should not skip isBeta challenge if in dev', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const beta = {
- dashedName: 'beta-challenge',
- isBeta: true,
- block: 'current-block'
- };
- const nextChallenge = {
- dashedName: 'next-challenge',
- block: 'current-block'
- };
- const entities = {
+ }
+ };
+ t.isEqual(
+ getNextChallenge('current-challenge', entities, { isDev: true }),
+ comingSoon
+ );
+ });
+ t.test('should skip isBeta challenge', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const beta = {
+ dashedName: 'beta-challenge',
+ isBeta: true,
+ block: 'current-block'
+ };
+ const nextChallenge = {
+ dashedName: 'next-challenge',
+ block: 'current-block'
+ };
+ const shouldBeNext = getNextChallenge(
+ 'current-challenge',
+ {
challenge: {
'current-challenge': currentChallenge,
'next-challenge': nextChallenge,
- 'beta-challenge': beta
+ 'beta-challenge': beta,
+ 'beta-challenge2': beta
},
block: {
'current-block': {
challenges: [
'current-challenge',
'beta-challenge',
+ 'beta-challenge2',
'next-challenge'
]
}
}
- };
- t.isEqual(
- getNextChallenge('current-challenge', entities, { isDev: true }),
- beta
- );
- });
- });
-
- t.test('getFirstChallengeOfNextBlock', t => {
- t.plan(8);
- t.test('should return falsey when current challenge is not found', t => {
- t.plan(1);
- const entities = {
- challenge: {},
- block: {}
- };
- t.notOk(
- getFirstChallengeOfNextBlock('non-existent-challenge', entities),
- `
- gitFirstChallengeOfNextBlock returned true value for non-existant
- challenge
- `
- );
- });
- t.test('should return falsey when current block is not found', t => {
- t.plan(1);
- const entities = {
- challenge: {
- 'current-challenge': {
- block: 'non-existent-block'
- }
- },
- block: {}
- };
- t.notOk(
- getFirstChallengeOfNextBlock('current-challenge', entities),
- `
- getFirstChallengeOfNextBlock did not returned true value block
- did non exist
- `
- );
- });
- t.test('should return falsey if no current superBlock found', t => {
- t.plan(1);
- const entities = {
- challenge: { 'current-challenge': { block: 'current-block' } },
- block: {
- 'current-block': {
- dashedName: 'current-block',
- superBlock: 'current-super-block'
- }
- },
- superBlock: {}
- };
- t.notOk(
- getFirstChallengeOfNextBlock('current-challenge', entities),
- `
- getFirstChallengeOfNextBlock returned a true value
- when superBlock is undefined
- `
- );
- });
- t.test('should return falsey when no next block found', t => {
- t.plan(1);
- const entities = {
- challenge: { 'current-challenge': { block: 'current-block' } },
- block: {
- 'current-block': {
- dashedName: 'current-block',
- superBlock: 'current-super-block'
- }
- },
- superBlock: {
- 'current-super-block': {
- blocks: [
- 'current-block',
- 'non-exitent-block'
- ]
- }
- }
- };
- t.notOk(
- getFirstChallengeOfNextBlock('current-challenge', entities),
- `
- getFirstChallengeOfNextBlock returned a value when next block
- does not exist
- `
- );
- });
- t.test('should return first challenge of next block', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const entities = {
- challenge: {
- [currentChallenge.dashedName]: currentChallenge,
- [firstChallenge.dashedName]: firstChallenge
- },
- block: {
- 'current-block': {
- dashedName: 'current-block',
- superBlock: 'current-super-block'
- },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'current-super-block',
- challenges: [ 'first-challenge' ]
- }
- },
- superBlock: {
- 'current-super-block': {
- dashedName: 'current-super-block',
- blocks: [ 'current-block', 'next-block' ]
- }
- }
- };
- t.equal(
- getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
- firstChallenge,
- 'getFirstChallengeOfNextBlock did not return the correct challenge'
- );
- });
- t.test('should skip coming soon challenge of next block', t => {
- t.plan(2);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const comingSoon = {
- dashedName: 'coming-soon',
- block: 'next-block',
- isComingSoon: true
- };
- const comingSoon2 = {
- dashedName: 'coming-soon2',
- block: 'next-block',
- isComingSoon: true
- };
- const entities = {
- challenge: {
- [currentChallenge.dashedName]: currentChallenge,
- [firstChallenge.dashedName]: firstChallenge,
- 'coming-soon': comingSoon,
- 'coming-soon2': comingSoon2
- },
- block: {
- 'current-block': {
- dashedName: 'current-block',
- superBlock: 'current-super-block'
- },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'current-super-block',
- challenges: [
- 'coming-soon',
- 'coming-soon2',
- 'first-challenge'
- ]
- }
- },
- superBlock: {
- 'current-super-block': {
- dashedName: 'current-super-block',
- blocks: [ 'current-block', 'next-block' ]
- }
- }
- };
- t.notEqual(
- getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
- comingSoon,
- 'getFirstChallengeOfNextBlock returned isComingSoon challenge'
- );
- t.equal(
- getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
- firstChallenge,
- 'getFirstChallengeOfNextBlock did not return the correct challenge'
- );
- });
- t.test('should not skip coming soon in dev mode', t => {
- t.plan(1);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const comingSoon = {
- dashedName: 'coming-soon',
- block: 'next-block',
- isComingSoon: true
- };
- const entities = {
- challenge: {
- [currentChallenge.dashedName]: currentChallenge,
- [firstChallenge.dashedName]: firstChallenge,
- 'coming-soon': comingSoon
- },
- block: {
- 'current-block': {
- dashedName: 'current-block',
- superBlock: 'current-super-block'
- },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'current-super-block',
- challenges: [
- 'coming-soon',
- 'first-challenge'
- ]
- }
- },
- superBlock: {
- 'current-super-block': {
- dashedName: 'current-super-block',
- blocks: [ 'current-block', 'next-block' ]
- }
- }
- };
- t.equal(
- getFirstChallengeOfNextBlock(
- currentChallenge.dashedName,
- entities,
- { isDev: true }
- ),
- comingSoon,
- 'getFirstChallengeOfNextBlock returned isComingSoon challenge'
- );
- });
- t.test('should skip block if all challenges are coming soon', t => {
- t.plan(2);
- const currentChallenge = {
- dashedName: 'current-challenge',
- block: 'current-block'
- };
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const comingSoon = {
- dashedName: 'coming-soon',
- block: 'coming-soon-block',
- isComingSoon: true
- };
- const comingSoon2 = {
- dashedName: 'coming-soon2',
- block: 'coming-soon-block',
- isComingSoon: true
- };
- const entities = {
- challenge: {
- [currentChallenge.dashedName]: currentChallenge,
- [firstChallenge.dashedName]: firstChallenge,
- [comingSoon.dashedName]: comingSoon,
- [comingSoon2.dashedName]: comingSoon2
- },
- block: {
- 'current-block': {
- dashedName: 'current-block',
- superBlock: 'current-super-block'
- },
- 'coming-soon-block': {
- dashedName: 'coming-soon-block',
- superBlock: 'current-super-block',
- challenges: [
- 'coming-soon',
- 'coming-soon2'
- ]
- },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'current-super-block',
- challenges: [
- 'first-challenge'
- ]
- }
- },
- superBlock: {
- 'current-super-block': {
- dashedName: 'current-super-block',
- blocks: [
- 'current-block',
- 'coming-soon-block',
- 'next-block'
- ]
- }
- }
- };
- t.notEqual(
- getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
- comingSoon,
- 'getFirstChallengeOfNextBlock returned isComingSoon challenge'
- );
- t.equal(
- getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
- firstChallenge,
- 'getFirstChallengeOfNextBlock did not return the correct challenge'
- );
- });
- });
-
- t.test('getFirstChallengeOfNextBlock', t => {
- t.plan(10);
- t.test('should return falsey if current challenge not found', t => {
- t.plan(1);
- const entities = {
- challenge: {}
- };
- t.notOk(
- getFirstChallengeOfNextSuperBlock('current-challenge', entities),
- );
- });
- t.test('should return falsey if current block not found', t => {
- t.plan(1);
- const entities = {
- challenge: { 'current-challenge': { block: 'current-block' } },
- block: {}
- };
- t.notOk(
- getFirstChallengeOfNextSuperBlock('current-challenge', entities)
- );
- });
- t.test('should return falsey if current superBlock is not found', t => {
- t.plan(1);
- const entities = {
- challenge: { 'current-challenge': { block: 'current-block' } },
- block: { 'current-block': { superBlock: 'current-super-block' } },
- superBlock: {}
- };
- t.notOk(
- getFirstChallengeOfNextSuperBlock('current-challenge', entities)
- );
- });
- t.test('should return falsey when last superBlock', t => {
- t.plan(1);
- const entities = {
- challenge: { 'current-challenge': { block: 'current-block' } },
- block: { 'current-block': { superBlock: 'current-super-block' } },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' }
- }
- };
- const superBlocks = [ 'current-super-block' ];
- t.notOk(getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks
- ));
- });
- t.test('should return falsey when last block of new superblock', t => {
- t.plan(1);
- const entities = {
- challenge: { 'current-challenge': { block: 'current-block' } },
- block: {
- 'current-block': {
- superBlock: 'current-super-block'
- }
- },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' },
- 'next-super-block': {
- dashedName: 'next-super-block',
- blocks: [
- 'first-block'
- ]
- }
- }
- };
- const superBlocks = [ 'current-super-block', 'next-super-block' ];
- t.notOk(getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks
- ));
- });
- t.test('should return first challenge of next superBlock', t => {
- t.plan(1);
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const entities = {
- challenge: {
- 'current-challenge': { block: 'current-block' },
- [firstChallenge.dashedName]: firstChallenge
- },
- block: {
- 'current-block': { superBlock: 'current-super-block' },
- 'next-block': {
- superBlock: 'next-super-block',
- challenges: [ 'first-challenge' ]
- }
- },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' },
- 'next-super-block': {
- dashedName: 'next-super-block',
- blocks: [ 'next-block' ]
- }
- }
- };
- const superBlocks = [ 'current-super-block', 'next-super-block' ];
- t.isEqual(
- getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks
- ),
- firstChallenge
- );
- });
- t.test('should skip coming soon challenge', t => {
- t.plan(1);
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const entities = {
- challenge: {
- 'current-challenge': { block: 'current-block' },
- [firstChallenge.dashedName]: firstChallenge,
- 'coming-soon': {
- dashedName: 'coming-soon',
- block: 'next-block',
- isComingSoon: true
- }
- },
- block: {
- 'current-block': { superBlock: 'current-super-block' },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'next-super-block',
- challenges: [ 'coming-soon', 'first-challenge' ]
- }
- },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' },
- 'next-super-block': {
- dashedName: 'next-super-block',
- blocks: [ 'next-block' ]
- }
- }
- };
- const superBlocks = [
- 'current-super-block',
- 'next-super-block'
- ];
- t.isEqual(
- getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks
- ),
- firstChallenge
- );
- });
- t.test('should not skip coming soon in dev mode', t => {
- t.plan(1);
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const comingSoon = {
- dashedName: 'coming-soon',
- block: 'next-block',
- isComingSoon: true
- };
- const entities = {
- challenge: {
- 'current-challenge': { block: 'current-block' },
- [firstChallenge.dashedName]: firstChallenge,
- 'coming-soon': comingSoon
- },
- block: {
- 'current-block': { superBlock: 'current-super-block' },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'next-super-block',
- challenges: [ 'coming-soon', 'first-challenge' ]
- }
- },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' },
- 'next-super-block': {
- dashedName: 'next-super-block',
- blocks: [ 'next-block' ]
- }
- }
- };
- const superBlocks = [
- 'current-super-block',
- 'next-super-block'
- ];
- t.isEqual(
- getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks,
- { isDev: true }
- ),
- comingSoon
- );
- });
- t.test('should skip coming soon block', t => {
- t.plan(1);
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const entities = {
- challenge: {
- 'current-challenge': { block: 'current-block' },
- [firstChallenge.dashedName]: firstChallenge,
- 'coming-soon': {
- dashedName: 'coming-soon',
- block: 'coming-soon-block',
- isComingSoon: true
- }
- },
- block: {
- 'current-block': { superBlock: 'current-super-block' },
- 'coming-soon-block': {
- dashedName: 'coming-soon-block',
- superBlock: 'next-super-block',
- challenges: [
- 'coming-soon'
- ]
- },
- 'next-block': {
- dashedName: 'next-block',
- superBlock: 'next-super-block',
- challenges: [ 'first-challenge' ]
- }
- },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' },
- 'next-super-block': {
- dashedName: 'next-super-block',
- blocks: [ 'coming-soon-block', 'next-block' ]
- }
- }
- };
- const superBlocks = [
- 'current-super-block',
- 'next-super-block'
- ];
- t.isEqual(
- getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks
- ),
- firstChallenge
- );
- });
- t.test('should skip coming soon super block', t => {
- t.plan(1);
- const firstChallenge = {
- dashedName: 'first-challenge',
- block: 'next-block'
- };
- const entities = {
- challenge: {
- 'current-challenge': { block: 'current-block' },
- [firstChallenge.dashedName]: firstChallenge,
- 'coming-soon': {
- dashedName: 'coming-soon',
- block: 'coming-soon-block',
- isComingSoon: true
- }
- },
- block: {
- 'current-block': { superBlock: 'current-super-block' },
- 'coming-soon-block': {
- dashedName: 'coming-soon-block',
- superBlock: 'coming-soon-super-block',
- challenges: [
- 'coming-soon'
- ]
- },
- 'next-block': {
- superBlock: 'next-super-block',
- dashedName: 'next-block',
- challenges: [ 'first-challenge' ]
- }
- },
- superBlock: {
- 'current-super-block': { dashedName: 'current-super-block' },
- 'coming-soon-super-block': {
- dashedName: 'coming-soon-super-block',
- blocks: [ 'coming-soon-block' ]
- },
- 'next-super-block': {
- dashedName: 'next-super-block',
- blocks: [ 'next-block' ]
- }
- }
- };
- const superBlocks = [
- 'current-super-block',
- 'coming-soon-super-block',
- 'next-super-block'
- ];
- t.isEqual(
- getFirstChallengeOfNextSuperBlock(
- 'current-challenge',
- entities,
- superBlocks
- ),
- firstChallenge
- );
- });
- });
- t.test('filterComingSoonBetaChallenge', t => {
- t.plan(4);
- t.test('should return true when not coming-soon/beta', t => {
- let isDev;
- t.ok(filterComingSoonBetaChallenge(isDev, {}));
- t.ok(filterComingSoonBetaChallenge(true, {}));
- t.end();
- });
- t.test('should return false when isComingSoon', t => {
- let isDev;
- t.notOk(filterComingSoonBetaChallenge(isDev, { isComingSoon: true }));
- t.end();
- });
- t.test('should return false when isBeta', t => {
- let isDev;
- t.notOk(filterComingSoonBetaChallenge(isDev, { isBeta: true }));
- t.end();
- });
- t.test('should always return true when in dev', t => {
- let isDev = true;
- t.ok(filterComingSoonBetaChallenge(isDev, { isBeta: true }));
- t.ok(filterComingSoonBetaChallenge(isDev, { isComingSoon: true }));
- t.ok(filterComingSoonBetaChallenge(
- isDev,
- { isBeta: true, isCompleted: true }
- ));
- t.end();
- });
- });
- t.test('filterComingSoonBetaFromEntities', t => {
- t.plan(2);
- t.test('should filter isBeta|coming-soon by default', t => {
- t.plan(2);
- const normalChallenge = { dashedName: 'normal-challenge' };
- const entities = {
- challenge: {
- 'coming-soon': {
- isComingSoon: true
- },
- 'is-beta': {
- isBeta: true
- },
- [normalChallenge.dashedName]: normalChallenge
- }
- };
- const actual = filterComingSoonBetaFromEntities(entities);
- t.isEqual(
- Object.keys(actual.challenge).length,
- 1,
- 'did not filter the correct amount of challenges'
- );
- t.isEqual(
- actual.challenge[normalChallenge.dashedName],
- normalChallenge,
- 'did not return the correct challenge'
- );
- });
- t.test('should not filter isBeta|coming-soon when isDev', t => {
- t.plan(1);
- const normalChallenge = { dashedName: 'normal-challenge' };
- const entities = {
- challenge: {
- 'coming-soon': {
- dashedName: 'coming-soon',
- isComingSoon: true
- },
- 'is-beta': {
- dashedName: 'is-beta',
- isBeta: true
- },
- 'is-both': {
- dashedName: 'is-both',
- isBeta: true
- },
- [normalChallenge.dashedName]: normalChallenge
- }
- };
- const actual = filterComingSoonBetaFromEntities(entities, true);
- t.isEqual(
- Object.keys(actual.challenge).length,
- 4,
- 'filtered challenges'
- );
- });
- });
- t.test('createMapUi', t => {
- t.plan(3);
- t.test('should return an `{}` when proper args not supplied', t => {
- t.plan(3);
- t.equal(
- Object.keys(createMapUi()).length,
- 0
- );
- t.equal(
- Object.keys(createMapUi({}, [])).length,
- 0
- );
- t.equal(
- Object.keys(createMapUi({ superBlock: {} }, [])).length,
- 0
- );
- });
- t.test('should return a map tree', t => {
- const expected = {
- children: [{
- name: 'superBlockA',
- children: [{
- name: 'blockA',
- children: [{
- name: 'challengeA'
- }]
- }]
- }]
- };
- const actual = createMapUi({
- superBlock: {
- superBlockA: {
- blocks: [
- 'blockA'
- ]
- }
- },
- block: {
- blockA: {
- challenges: [
- 'challengeA'
- ]
- }
- }
- },
- ['superBlockA'],
- { challengeA: 'ChallengeA title'}
+ }
);
- t.plan(3);
- t.equal(actual.children[0].name, expected.children[0].name);
- t.equal(
- actual.children[0].children[0].name,
- expected.children[0].children[0].name
- );
- t.equal(
- actual.children[0].children[0].children[0].name,
- expected.children[0].children[0].children[0].name
- );
- });
- t.test('should protect against malformed data', t => {
- t.plan(2);
- t.equal(
- createMapUi({
- superBlock: {},
- block: {
- blockA: {
- challenges: [
- 'challengeA'
- ]
- }
- }
- }, ['superBlockA']).children[0].children.length,
- 0
- );
- t.equal(
- createMapUi({
- superBlock: {
- superBlockA: {
- blocks: [
- 'blockA'
- ]
- }
- },
- block: {}
- }, ['superBlockA']).children[0].children[0].children.length,
- 0
- );
- });
+ t.isEqual(shouldBeNext, nextChallenge);
});
- t.test('traverseMapUi', t => {
- t.test('should return tree', t => {
- t.plan(2);
- const expectedTree = {};
- const actaulTree = traverseMapUi(expectedTree, tree => {
- t.equal(tree, expectedTree);
- return tree;
- });
- t.equal(actaulTree, expectedTree);
- });
- t.test('should hit every node', t => {
- t.plan(4);
- const expected = { children: [{ children: [{}] }] };
- const spy = sinon.spy(t => t);
- spy.withArgs(expected);
- spy.withArgs(expected.children[0]);
- spy.withArgs(expected.children[0].children[0]);
- traverseMapUi(expected, spy);
- t.equal(spy.callCount, 3);
- t.ok(spy.withArgs(expected).calledOnce, 'foo');
- t.ok(spy.withArgs(expected.children[0]).calledOnce, 'bar');
- t.ok(spy.withArgs(expected.children[0].children[0]).calledOnce, 'baz');
- });
- t.test('should create new object when children change', t => {
- t.plan(9);
- const expected = { children: [{ bar: true }, {}] };
- const actual = traverseMapUi(expected, node => ({ ...node, foo: true }));
- t.notEqual(actual, expected);
- t.notEqual(actual.children, expected.children);
- t.notEqual(actual.children[0], expected.children[0]);
- t.notEqual(actual.children[1], expected.children[1]);
- t.equal(actual.children[0].bar, expected.children[0].bar);
- t.notOk(expected.children[0].foo);
- t.notOk(expected.children[1].foo);
- t.true(actual.children[0].foo);
- t.true(actual.children[1].foo);
- });
- });
- t.test('getNode', t => {
- t.test('should return node', t => {
- t.plan(1);
- const expected = { name: 'foo' };
- const tree = { children: [{ name: 'notfoo' }, expected ] };
- const actual = getNode(tree, 'foo');
- t.equal(expected, actual);
- });
- t.test('should returned undefined if not found', t => {
- t.plan(1);
- const tree = {
- children: [ { name: 'foo' }, { children: [ { name: 'bar' } ] } ]
- };
- const actual = getNode(tree, 'baz');
- t.notOk(actual);
- });
- });
- t.test('updateSingleNode', t => {
- t.test('should update single node', t => {
- const expected = { name: 'foo' };
- const untouched = { name: 'notFoo' };
- const actual = updateSingleNode(
- { children: [ untouched, expected ] },
- 'foo',
- node => ({ ...node, tag: true })
- );
- t.plan(4);
- t.ok(actual.children[1].tag);
- t.equal(actual.children[1].name, expected.name);
- t.notEqual(actual.children[1], expected);
- t.equal(actual.children[0], untouched);
- });
- });
- t.test('toggleThisPanel', t => {
- t.test('should update single node', t => {
- const expected = { name: 'foo', isOpen: true };
- const actual = toggleThisPanel(
- { children: [ { name: 'foo', isOpen: false }] },
- 'foo'
- );
- t.plan(1);
- t.deepLooseEqual(actual.children[0], expected);
- });
- });
- t.test('toggleAllPanels', t => {
- t.test('should add `isOpen: true` to every node without children', t => {
- const expected = {
- isOpen: true,
- children: [{
- isOpen: true,
- children: [{}, {}]
- }]
- };
- const actual = expandAllPanels({ children: [{ children: [{}, {}] }] });
- t.plan(1);
- t.deepLooseEqual(actual, expected);
- });
- t.test('should add `isOpen: false` to every node without children', t => {
- const leaf = {};
- const expected = {
- isOpen: false,
- children: [{
- isOpen: false,
- children: [{}, leaf]
- }]
- };
- const actual = collapseAllPanels(
- { isOpen: true, children: [{ children: [{}, leaf]}]},
- );
- t.plan(2);
- t.deepLooseEqual(actual, expected);
- t.equal(actual.children[0].children[1], leaf);
- });
- });
- t.test('applyFilterToMap', t => {
- t.test('should not touch child that is already hidden', t => {
- t.plan(1);
- const expected = { name: 'bar', isHidden: true };
- const actual = applyFilterToMap(
- expected,
- /foo/
- );
- t.equal(actual, expected);
- });
- t.test('should update child that is hidden', t => {
- t.plan(1);
- const expected = { title: 'bar', isHidden: false };
- const input = { title: 'bar', isHidden: true };
- const actual = applyFilterToMap(input, /bar/);
- t.deepLooseEqual(actual, expected);
- });
- t.test('should unhide child that matches filter regex', t => {
- t.plan(1);
- const expected = { title: 'foo' };
- const actual = applyFilterToMap({ title: 'foo' }, /foo/);
- t.deepLooseEqual(actual, expected);
- });
- t.test('should hide child that does not match filter', t => {
- t.plan(1);
- const expected = { title: 'bar', isHidden: true };
- const actual = applyFilterToMap({ title: 'bar' }, /foo/);
- t.deepLooseEqual(actual, expected);
- });
- t.test('should not touch node that is already hidden', t => {
- t.plan(1);
- const expected = {
- name: 'bar',
- isHidden: true,
- children: [
- { name: 'baz', isHidden: true },
- { name: 'baz2', isHidden: true }
- ]
- };
- const actual = applyFilterToMap(expected, /foo/);
- t.equal(actual, expected);
- });
- t.test('should not touch node that is unhidden', t => {
- t.plan(1);
- const expected = {
- name: 'bar',
- isHidden: false,
- children: [
- { title: 'baz', isHidden: true },
- { title: 'foo', isHidden: false }
- ]
- };
- const actual = applyFilterToMap(expected, /foo/);
- t.equal(actual, expected);
- });
- t.test('should hide node if all children are hidden', t => {
- t.plan(1);
- const input = {
- name: 'bar',
- isHidden: false,
- children: [
- { name: 'baz' },
- { name: 'baz2', isHidden: false }
- ]
- };
- const expected = {
- name: 'bar',
- isHidden: true,
- children: [
- { name: 'baz', isHidden: true },
- { name: 'baz2', isHidden: true }
- ]
- };
- const actual = applyFilterToMap(input, /foo/);
- t.deepLooseEqual(actual, expected);
- });
- t.test('should unhide node some children unhidden', t => {
- t.plan(1);
- const input = {
- name: 'bar',
- isHidden: true,
- children: [
- { title: 'baz', isHidden: true },
- { title: 'foo', isHidden: false }
- ]
- };
- const expected = {
- name: 'bar',
- isHidden: false,
- children: [
- { title: 'baz', isHidden: true },
- { title: 'foo', isHidden: false }
- ]
- };
- const actual = applyFilterToMap(input, /foo/);
- t.deepLooseEqual(actual, expected);
- });
- });
- t.test('unfilterMapUi', t => {
- t.test('should not touch node that is already hidden', t => {
- const expected = { isHidden: false };
- const actual = unfilterMapUi(expected);
- t.plan(1);
- t.equal(actual, expected);
- });
- t.test('should update node that is not hidden', t => {
- const expected = { isHidden: false };
- const input = { isHidden: true };
- const actual = unfilterMapUi(input);
- t.plan(2);
- t.notEqual(actual, input);
- t.deepLooseEqual(actual, expected);
- });
+ t.test('should not skip isBeta challenge if in dev', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const beta = {
+ dashedName: 'beta-challenge',
+ isBeta: true,
+ block: 'current-block'
+ };
+ const nextChallenge = {
+ dashedName: 'next-challenge',
+ block: 'current-block'
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': currentChallenge,
+ 'next-challenge': nextChallenge,
+ 'beta-challenge': beta
+ },
+ block: {
+ 'current-block': {
+ challenges: [
+ 'current-challenge',
+ 'beta-challenge',
+ 'next-challenge'
+ ]
+ }
+ }
+ };
+ t.isEqual(
+ getNextChallenge('current-challenge', entities, { isDev: true }),
+ beta
+ );
+ });
+});
+
+test('getFirstChallengeOfNextBlock', t => {
+ t.plan(8);
+ t.test('should return falsey when current challenge is not found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: {},
+ block: {}
+ };
+ t.notOk(
+ getFirstChallengeOfNextBlock('non-existent-challenge', entities),
+ `
+ gitFirstChallengeOfNextBlock returned true value for non-existant
+ challenge
+ `
+ );
+ });
+ t.test('should return falsey when current block is not found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: {
+ 'current-challenge': {
+ block: 'non-existent-block'
+ }
+ },
+ block: {}
+ };
+ t.notOk(
+ getFirstChallengeOfNextBlock('current-challenge', entities),
+ `
+ getFirstChallengeOfNextBlock did not returned true value block
+ did non exist
+ `
+ );
+ });
+ t.test('should return falsey if no current superBlock found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: { 'current-challenge': { block: 'current-block' } },
+ block: {
+ 'current-block': {
+ dashedName: 'current-block',
+ superBlock: 'current-super-block'
+ }
+ },
+ superBlock: {}
+ };
+ t.notOk(
+ getFirstChallengeOfNextBlock('current-challenge', entities),
+ `
+ getFirstChallengeOfNextBlock returned a true value
+ when superBlock is undefined
+ `
+ );
+ });
+ t.test('should return falsey when no next block found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: { 'current-challenge': { block: 'current-block' } },
+ block: {
+ 'current-block': {
+ dashedName: 'current-block',
+ superBlock: 'current-super-block'
+ }
+ },
+ superBlock: {
+ 'current-super-block': {
+ blocks: [
+ 'current-block',
+ 'non-exitent-block'
+ ]
+ }
+ }
+ };
+ t.notOk(
+ getFirstChallengeOfNextBlock('current-challenge', entities),
+ `
+ getFirstChallengeOfNextBlock returned a value when next block
+ does not exist
+ `
+ );
+ });
+ t.test('should return first challenge of next block', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const entities = {
+ challenge: {
+ [currentChallenge.dashedName]: currentChallenge,
+ [firstChallenge.dashedName]: firstChallenge
+ },
+ block: {
+ 'current-block': {
+ dashedName: 'current-block',
+ superBlock: 'current-super-block'
+ },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'current-super-block',
+ challenges: [ 'first-challenge' ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': {
+ dashedName: 'current-super-block',
+ blocks: [ 'current-block', 'next-block' ]
+ }
+ }
+ };
+ t.equal(
+ getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
+ firstChallenge,
+ 'getFirstChallengeOfNextBlock did not return the correct challenge'
+ );
+ });
+ t.test('should skip coming soon challenge of next block', t => {
+ t.plan(2);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const comingSoon = {
+ dashedName: 'coming-soon',
+ block: 'next-block',
+ isComingSoon: true
+ };
+ const comingSoon2 = {
+ dashedName: 'coming-soon2',
+ block: 'next-block',
+ isComingSoon: true
+ };
+ const entities = {
+ challenge: {
+ [currentChallenge.dashedName]: currentChallenge,
+ [firstChallenge.dashedName]: firstChallenge,
+ 'coming-soon': comingSoon,
+ 'coming-soon2': comingSoon2
+ },
+ block: {
+ 'current-block': {
+ dashedName: 'current-block',
+ superBlock: 'current-super-block'
+ },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'current-super-block',
+ challenges: [
+ 'coming-soon',
+ 'coming-soon2',
+ 'first-challenge'
+ ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': {
+ dashedName: 'current-super-block',
+ blocks: [ 'current-block', 'next-block' ]
+ }
+ }
+ };
+ t.notEqual(
+ getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
+ comingSoon,
+ 'getFirstChallengeOfNextBlock returned isComingSoon challenge'
+ );
+ t.equal(
+ getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
+ firstChallenge,
+ 'getFirstChallengeOfNextBlock did not return the correct challenge'
+ );
+ });
+ t.test('should not skip coming soon in dev mode', t => {
+ t.plan(1);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const comingSoon = {
+ dashedName: 'coming-soon',
+ block: 'next-block',
+ isComingSoon: true
+ };
+ const entities = {
+ challenge: {
+ [currentChallenge.dashedName]: currentChallenge,
+ [firstChallenge.dashedName]: firstChallenge,
+ 'coming-soon': comingSoon
+ },
+ block: {
+ 'current-block': {
+ dashedName: 'current-block',
+ superBlock: 'current-super-block'
+ },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'current-super-block',
+ challenges: [
+ 'coming-soon',
+ 'first-challenge'
+ ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': {
+ dashedName: 'current-super-block',
+ blocks: [ 'current-block', 'next-block' ]
+ }
+ }
+ };
+ t.equal(
+ getFirstChallengeOfNextBlock(
+ currentChallenge.dashedName,
+ entities,
+ { isDev: true }
+ ),
+ comingSoon,
+ 'getFirstChallengeOfNextBlock returned isComingSoon challenge'
+ );
+ });
+ t.test('should skip block if all challenges are coming soon', t => {
+ t.plan(2);
+ const currentChallenge = {
+ dashedName: 'current-challenge',
+ block: 'current-block'
+ };
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const comingSoon = {
+ dashedName: 'coming-soon',
+ block: 'coming-soon-block',
+ isComingSoon: true
+ };
+ const comingSoon2 = {
+ dashedName: 'coming-soon2',
+ block: 'coming-soon-block',
+ isComingSoon: true
+ };
+ const entities = {
+ challenge: {
+ [currentChallenge.dashedName]: currentChallenge,
+ [firstChallenge.dashedName]: firstChallenge,
+ [comingSoon.dashedName]: comingSoon,
+ [comingSoon2.dashedName]: comingSoon2
+ },
+ block: {
+ 'current-block': {
+ dashedName: 'current-block',
+ superBlock: 'current-super-block'
+ },
+ 'coming-soon-block': {
+ dashedName: 'coming-soon-block',
+ superBlock: 'current-super-block',
+ challenges: [
+ 'coming-soon',
+ 'coming-soon2'
+ ]
+ },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'current-super-block',
+ challenges: [
+ 'first-challenge'
+ ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': {
+ dashedName: 'current-super-block',
+ blocks: [
+ 'current-block',
+ 'coming-soon-block',
+ 'next-block'
+ ]
+ }
+ }
+ };
+ t.notEqual(
+ getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
+ comingSoon,
+ 'getFirstChallengeOfNextBlock returned isComingSoon challenge'
+ );
+ t.equal(
+ getFirstChallengeOfNextBlock(currentChallenge.dashedName, entities),
+ firstChallenge,
+ 'getFirstChallengeOfNextBlock did not return the correct challenge'
+ );
+ });
+});
+
+test('getFirstChallengeOfNextBlock', t => {
+ t.plan(10);
+ t.test('should return falsey if current challenge not found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: {}
+ };
+ t.notOk(
+ getFirstChallengeOfNextSuperBlock('current-challenge', entities),
+ );
+ });
+ t.test('should return falsey if current block not found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: { 'current-challenge': { block: 'current-block' } },
+ block: {}
+ };
+ t.notOk(
+ getFirstChallengeOfNextSuperBlock('current-challenge', entities)
+ );
+ });
+ t.test('should return falsey if current superBlock is not found', t => {
+ t.plan(1);
+ const entities = {
+ challenge: { 'current-challenge': { block: 'current-block' } },
+ block: { 'current-block': { superBlock: 'current-super-block' } },
+ superBlock: {}
+ };
+ t.notOk(
+ getFirstChallengeOfNextSuperBlock('current-challenge', entities)
+ );
+ });
+ t.test('should return falsey when last superBlock', t => {
+ t.plan(1);
+ const entities = {
+ challenge: { 'current-challenge': { block: 'current-block' } },
+ block: { 'current-block': { superBlock: 'current-super-block' } },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' }
+ }
+ };
+ const superBlocks = [ 'current-super-block' ];
+ t.notOk(getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks
+ ));
+ });
+ t.test('should return falsey when last block of new superblock', t => {
+ t.plan(1);
+ const entities = {
+ challenge: { 'current-challenge': { block: 'current-block' } },
+ block: {
+ 'current-block': {
+ superBlock: 'current-super-block'
+ }
+ },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' },
+ 'next-super-block': {
+ dashedName: 'next-super-block',
+ blocks: [
+ 'first-block'
+ ]
+ }
+ }
+ };
+ const superBlocks = [ 'current-super-block', 'next-super-block' ];
+ t.notOk(getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks
+ ));
+ });
+ t.test('should return first challenge of next superBlock', t => {
+ t.plan(1);
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': { block: 'current-block' },
+ [firstChallenge.dashedName]: firstChallenge
+ },
+ block: {
+ 'current-block': { superBlock: 'current-super-block' },
+ 'next-block': {
+ superBlock: 'next-super-block',
+ challenges: [ 'first-challenge' ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' },
+ 'next-super-block': {
+ dashedName: 'next-super-block',
+ blocks: [ 'next-block' ]
+ }
+ }
+ };
+ const superBlocks = [ 'current-super-block', 'next-super-block' ];
+ t.isEqual(
+ getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks
+ ),
+ firstChallenge
+ );
+ });
+ t.test('should skip coming soon challenge', t => {
+ t.plan(1);
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': { block: 'current-block' },
+ [firstChallenge.dashedName]: firstChallenge,
+ 'coming-soon': {
+ dashedName: 'coming-soon',
+ block: 'next-block',
+ isComingSoon: true
+ }
+ },
+ block: {
+ 'current-block': { superBlock: 'current-super-block' },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'next-super-block',
+ challenges: [ 'coming-soon', 'first-challenge' ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' },
+ 'next-super-block': {
+ dashedName: 'next-super-block',
+ blocks: [ 'next-block' ]
+ }
+ }
+ };
+ const superBlocks = [
+ 'current-super-block',
+ 'next-super-block'
+ ];
+ t.isEqual(
+ getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks
+ ),
+ firstChallenge
+ );
+ });
+ t.test('should not skip coming soon in dev mode', t => {
+ t.plan(1);
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const comingSoon = {
+ dashedName: 'coming-soon',
+ block: 'next-block',
+ isComingSoon: true
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': { block: 'current-block' },
+ [firstChallenge.dashedName]: firstChallenge,
+ 'coming-soon': comingSoon
+ },
+ block: {
+ 'current-block': { superBlock: 'current-super-block' },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'next-super-block',
+ challenges: [ 'coming-soon', 'first-challenge' ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' },
+ 'next-super-block': {
+ dashedName: 'next-super-block',
+ blocks: [ 'next-block' ]
+ }
+ }
+ };
+ const superBlocks = [
+ 'current-super-block',
+ 'next-super-block'
+ ];
+ t.isEqual(
+ getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks,
+ { isDev: true }
+ ),
+ comingSoon
+ );
+ });
+ t.test('should skip coming soon block', t => {
+ t.plan(1);
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': { block: 'current-block' },
+ [firstChallenge.dashedName]: firstChallenge,
+ 'coming-soon': {
+ dashedName: 'coming-soon',
+ block: 'coming-soon-block',
+ isComingSoon: true
+ }
+ },
+ block: {
+ 'current-block': { superBlock: 'current-super-block' },
+ 'coming-soon-block': {
+ dashedName: 'coming-soon-block',
+ superBlock: 'next-super-block',
+ challenges: [
+ 'coming-soon'
+ ]
+ },
+ 'next-block': {
+ dashedName: 'next-block',
+ superBlock: 'next-super-block',
+ challenges: [ 'first-challenge' ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' },
+ 'next-super-block': {
+ dashedName: 'next-super-block',
+ blocks: [ 'coming-soon-block', 'next-block' ]
+ }
+ }
+ };
+ const superBlocks = [
+ 'current-super-block',
+ 'next-super-block'
+ ];
+ t.isEqual(
+ getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks
+ ),
+ firstChallenge
+ );
+ });
+ t.test('should skip coming soon super block', t => {
+ t.plan(1);
+ const firstChallenge = {
+ dashedName: 'first-challenge',
+ block: 'next-block'
+ };
+ const entities = {
+ challenge: {
+ 'current-challenge': { block: 'current-block' },
+ [firstChallenge.dashedName]: firstChallenge,
+ 'coming-soon': {
+ dashedName: 'coming-soon',
+ block: 'coming-soon-block',
+ isComingSoon: true
+ }
+ },
+ block: {
+ 'current-block': { superBlock: 'current-super-block' },
+ 'coming-soon-block': {
+ dashedName: 'coming-soon-block',
+ superBlock: 'coming-soon-super-block',
+ challenges: [
+ 'coming-soon'
+ ]
+ },
+ 'next-block': {
+ superBlock: 'next-super-block',
+ dashedName: 'next-block',
+ challenges: [ 'first-challenge' ]
+ }
+ },
+ superBlock: {
+ 'current-super-block': { dashedName: 'current-super-block' },
+ 'coming-soon-super-block': {
+ dashedName: 'coming-soon-super-block',
+ blocks: [ 'coming-soon-block' ]
+ },
+ 'next-super-block': {
+ dashedName: 'next-super-block',
+ blocks: [ 'next-block' ]
+ }
+ }
+ };
+ const superBlocks = [
+ 'current-super-block',
+ 'coming-soon-super-block',
+ 'next-super-block'
+ ];
+ t.isEqual(
+ getFirstChallengeOfNextSuperBlock(
+ 'current-challenge',
+ entities,
+ superBlocks
+ ),
+ firstChallenge
+ );
});
});
diff --git a/common/app/routes/challenges/views/backend/Back-End.jsx b/common/app/routes/challenges/views/backend/Back-End.jsx
index fd062e4e14..41c32434f0 100644
--- a/common/app/routes/challenges/views/backend/Back-End.jsx
+++ b/common/app/routes/challenges/views/backend/Back-End.jsx
@@ -11,14 +11,20 @@ import ChallengeTitle from '../../Challenge-Title.jsx';
import SolutionInput from '../../Solution-Input.jsx';
import TestSuite from '../../Test-Suite.jsx';
import Output from '../../Output.jsx';
-import { submitChallenge, executeChallenge } from '../../redux/actions.js';
-import { challengeSelector } from '../../redux/selectors.js';
+import {
+ submitChallenge,
+ executeChallenge,
+ testsSelector,
+ outputSelector
+} from '../../redux';
import { descriptionRegex } from '../../utils.js';
+
import {
createFormValidator,
isValidURL,
makeRequired
} from '../../../../utils/form.js';
+import { challengeSelector } from '../../../../redux';
// provided by redux form
const reduxFormPropTypes = {
@@ -47,15 +53,13 @@ const fieldValidators = {
const mapStateToProps = createSelector(
challengeSelector,
- state => state.challengesApp.output,
- state => state.challengesApp.tests,
+ outputSelector,
+ testsSelector,
(
{
- challenge: {
- id,
- title,
- description
- } = {}
+ id,
+ title,
+ description
},
output,
tests
@@ -74,7 +78,6 @@ const mapDispatchToActions = {
};
export class BackEnd extends PureComponent {
-
renderDescription(description) {
if (!Array.isArray(description)) {
return null;
diff --git a/common/app/routes/challenges/views/backend/Show.jsx b/common/app/routes/challenges/views/backend/Show.jsx
new file mode 100644
index 0000000000..477083c7e1
--- /dev/null
+++ b/common/app/routes/challenges/views/backend/Show.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import BackEnd from './Back-End.jsx';
+import { types } from '../../redux';
+import Panes from '../../../../Panes';
+import _Map from '../../../../Map';
+import ChildContainer from '../../../../Child-Container.jsx';
+
+const propTypes = {};
+
+export const panesMap = {
+ [types.toggleMap]: 'Map',
+ [types.toggleMain]: 'Main'
+};
+
+const nameToComponentDef = {
+ Map: {
+ Component: _Map,
+ defaultSize: 25
+ },
+ Main: {
+ Component: BackEnd,
+ defaultSize: 50
+ }
+};
+
+export default function ShowBackEnd() {
+ return (
+
+
+
+ );
+}
+
+ShowBackEnd.displayName = 'ShowBackEnd';
+ShowBackEnd.propTypes = propTypes;
diff --git a/common/app/routes/challenges/views/backend/index.js b/common/app/routes/challenges/views/backend/index.js
index 9da7aec1c3..f8a8115a06 100644
--- a/common/app/routes/challenges/views/backend/index.js
+++ b/common/app/routes/challenges/views/backend/index.js
@@ -1 +1 @@
-export { default } from './Back-End.jsx';
+export { default, panesMap } from './Show.jsx';
diff --git a/common/app/routes/challenges/views/classic/Classic-Modal.jsx b/common/app/routes/challenges/views/classic/Classic-Modal.jsx
deleted file mode 100644
index 945d4161c5..0000000000
--- a/common/app/routes/challenges/views/classic/Classic-Modal.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React, { PropTypes } from 'react';
-import { Button, Modal } from 'react-bootstrap';
-import PureComponent from 'react-pure-render/component';
-import FontAwesome from 'react-fontawesome';
-
-import ns from './ns.json';
-
-const propTypes = {
- close: PropTypes.func,
- open: PropTypes.bool.isRequired,
- submitChallenge: PropTypes.func.isRequired,
- successMessage: PropTypes.string.isRequired
-};
-
-export default class ClassicModal extends PureComponent {
- constructor(...props) {
- super(...props);
- this.handleKeyDown = this.handleKeyDown.bind(this);
- }
-
- handleKeyDown(e) {
- const { open, submitChallenge } = this.props;
- if (
- e.keyCode === 13 &&
- (e.ctrlKey || e.meta) &&
- open
- ) {
- e.preventDefault();
- submitChallenge();
- }
- }
-
- render() {
- const {
- close,
- open,
- submitChallenge,
- successMessage
- } = this.props;
- return (
-
-
- { successMessage }
-
-
-
-
-
-
-
-
- );
- }
-}
-
-ClassicModal.displayName = 'ClassicModal';
-ClassicModal.propTypes = propTypes;
diff --git a/common/app/routes/challenges/views/classic/Classic.jsx b/common/app/routes/challenges/views/classic/Classic.jsx
deleted file mode 100644
index 4a1e7da1e0..0000000000
--- a/common/app/routes/challenges/views/classic/Classic.jsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import React, { PropTypes } from 'react';
-import { connect } from 'react-redux';
-import { Row, Col } from 'react-bootstrap';
-import { createSelector } from 'reselect';
-import PureComponent from 'react-pure-render/component';
-
-import Editor from './Editor.jsx';
-import SidePanel from './Side-Panel.jsx';
-import Preview from './Preview.jsx';
-import BugModal from '../../Bug-Modal.jsx';
-import ClassicModal from './Classic-Modal.jsx';
-import { challengeSelector } from '../../redux/selectors';
-import {
- executeChallenge,
- updateFile,
- loadCode,
- submitChallenge,
- closeChallengeModal,
- updateSuccessMessage
-} from '../../redux/actions';
-import { randomCompliment } from '../../../../utils/get-words';
-
-const mapStateToProps = createSelector(
- challengeSelector,
- state => state.challengesApp.id,
- state => state.challengesApp.tests,
- state => state.challengesApp.files,
- state => state.challengesApp.key,
- state => state.challengesApp.isChallengeModalOpen,
- state => state.challengesApp.successMessage,
- (
- { showPreview, mode },
- id,
- tests,
- files = {},
- key = '',
- isChallengeModalOpen,
- successMessage,
- ) => ({
- id,
- content: files[key] && files[key].contents || '',
- file: files[key],
- showPreview,
- mode,
- tests,
- isChallengeModalOpen,
- successMessage
- })
-);
-
-const bindableActions = {
- executeChallenge,
- updateFile,
- loadCode,
- submitChallenge,
- closeChallengeModal,
- updateSuccessMessage
-};
-
-const propTypes = {
- closeChallengeModal: PropTypes.func,
- content: PropTypes.string,
- executeChallenge: PropTypes.func,
- file: PropTypes.object,
- id: PropTypes.string,
- isChallengeModalOpen: PropTypes.bool,
- loadCode: PropTypes.func,
- mode: PropTypes.string,
- showPreview: PropTypes.bool,
- submitChallenge: PropTypes.func,
- successMessage: PropTypes.string,
- updateFile: PropTypes.func,
- updateSuccessMessage: PropTypes.func
-};
-
-export class Challenge extends PureComponent {
-
- componentDidMount() {
- this.props.loadCode();
- this.props.updateSuccessMessage(randomCompliment());
- window.scrollTo(0, 0);
- }
-
- componentWillReceiveProps(nextProps) {
- if (this.props.id !== nextProps.id) {
- this.props.loadCode();
- this.props.updateSuccessMessage(randomCompliment());
- }
- }
-
- renderPreview(showPreview) {
- if (!showPreview) {
- return null;
- }
- return (
-
-
-
- );
- }
-
- render() {
- const {
- content,
- updateFile,
- file,
- mode,
- showPreview,
- executeChallenge,
- submitChallenge,
- successMessage,
- isChallengeModalOpen,
- closeChallengeModal
- } = this.props;
-
- return (
-
-
-
-
-
- updateFile(content, file) }
- />
-
- { this.renderPreview(showPreview) }
-
-
-
- );
- }
-}
-
-Challenge.displayName = 'Challenge';
-Challenge.propTypes = propTypes;
-
-export default connect(mapStateToProps, bindableActions)(Challenge);
diff --git a/common/app/routes/challenges/views/classic/Editor.jsx b/common/app/routes/challenges/views/classic/Editor.jsx
index 3424d5a547..1484eca552 100644
--- a/common/app/routes/challenges/views/classic/Editor.jsx
+++ b/common/app/routes/challenges/views/classic/Editor.jsx
@@ -1,54 +1,67 @@
-import { Subject } from 'rx';
-import React, { PropTypes } from 'react';
+import React, { PureComponent, PropTypes } from 'react';
+import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import Codemirror from 'react-codemirror';
import NoSSR from 'react-no-ssr';
-import PureComponent from 'react-pure-render/component';
import MouseTrap from 'mousetrap';
import ns from './ns.json';
import CodeMirrorSkeleton from '../../Code-Mirror-Skeleton.jsx';
+import {
+ executeChallenge,
+ classicEditorUpdated,
-const editorDebounceTimeout = 750;
+ challengeMetaSelector,
+ filesSelector,
+ keySelector
+} from '../../redux';
const options = {
- lint: {esversion: 6},
+ lint: { esversion: 6 },
lineNumbers: true,
mode: 'javascript',
- theme: 'monokai',
+ theme: 'freecodecamp',
runnable: true,
matchBrackets: true,
autoCloseBrackets: true,
scrollbarStyle: 'null',
lineWrapping: true,
- gutters: ['CodeMirror-lint-markers']
+ gutters: [ 'CodeMirror-lint-markers' ]
};
-const defaultProps = {
- content: '// Happy Coding!',
- mode: 'javascript'
+const mapStateToProps = createSelector(
+ filesSelector,
+ challengeMetaSelector,
+ keySelector,
+ (
+ files = {},
+ { mode = 'javascript'},
+ key
+ ) => ({
+ content: files[key] && files[key].contents || '// Happy Coding!',
+ file: files[key],
+ mode
+ })
+);
+
+const mapDispatchToProps = {
+ executeChallenge,
+ classicEditorUpdated
};
const propTypes = {
+ classicEditorUpdated: PropTypes.func.isRequired,
content: PropTypes.string,
- executeChallenge: PropTypes.func,
- mode: PropTypes.string,
- updateFile: PropTypes.func
+ executeChallenge: PropTypes.func.isRequired,
+ mode: PropTypes.string
};
-export default class Editor extends PureComponent {
- constructor(...args) {
- super(...args);
- this._editorContent$ = new Subject();
- this.handleChange = this.handleChange.bind(this);
- }
-
+export class Editor extends PureComponent {
createOptions = createSelector(
- state => state.options,
state => state.executeChallenge,
state => state.mode,
- (options, executeChallenge, mode) => ({
+ (executeChallenge, mode) => ({
...options,
mode,
extraKeys: {
@@ -88,46 +101,28 @@ export default class Editor extends PureComponent {
);
componentDidMount() {
- const { updateFile = (() => {}) } = this.props;
- this._subscription = this._editorContent$
- .debounce(editorDebounceTimeout)
- .distinctUntilChanged()
- .subscribe(
- updateFile,
- err => { throw err; }
- );
-
MouseTrap.bind('e', () => {
this.refs.editor.focus();
}, 'keyup');
}
componentWillUnmount() {
- if (this._subscription) {
- this._subscription.dispose();
- this._subscription = null;
- }
MouseTrap.unbind('e', 'keyup');
}
- handleChange(value) {
- if (this._subscription) {
- this._editorContent$.onNext(value);
- }
- }
-
render() {
const {
content,
executeChallenge,
+ classicEditorUpdated,
mode
} = this.props;
return (
}>
@@ -137,6 +132,10 @@ export default class Editor extends PureComponent {
}
}
-Editor.defaultProps = defaultProps;
Editor.displayName = 'Editor';
Editor.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Editor);
diff --git a/common/app/routes/challenges/views/classic/Show.jsx b/common/app/routes/challenges/views/classic/Show.jsx
new file mode 100644
index 0000000000..09b9bfc409
--- /dev/null
+++ b/common/app/routes/challenges/views/classic/Show.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+
+import SidePanel from './Side-Panel.jsx';
+import Editor from './Editor.jsx';
+import Preview from './Preview.jsx';
+import { types } from '../../redux';
+import Panes from '../../../../Panes';
+import _Map from '../../../../Map';
+import ChildContainer from '../../../../Child-Container.jsx';
+
+const propTypes = {};
+
+export const panesMap = {
+ [types.toggleMap]: 'Map',
+ [types.toggleSidePanel]: 'Side Panel',
+ [types.toggleClassicEditor]: 'Editor',
+ [types.togglePreview]: 'Preview'
+};
+
+const nameToComponent = {
+ Map: {
+ Component: _Map
+ },
+ 'Side Panel': {
+ Component: SidePanel
+ },
+ Editor: {
+ Component: Editor
+ },
+ Preview: {
+ Component: Preview
+ }
+};
+
+export default function ShowClassic() {
+ return (
+
+
+
+ );
+}
+
+ShowClassic.displayName = 'ShowClassic';
+ShowClassic.propTypes = propTypes;
diff --git a/common/app/routes/challenges/views/classic/Side-Panel.jsx b/common/app/routes/challenges/views/classic/Side-Panel.jsx
index 3d928a85b2..34532be1ca 100644
--- a/common/app/routes/challenges/views/classic/Side-Panel.jsx
+++ b/common/app/routes/challenges/views/classic/Side-Panel.jsx
@@ -7,19 +7,27 @@ import { Col, Row } from 'react-bootstrap';
import ns from './ns.json';
+import ToolPanel from './Tool-Panel.jsx';
import ChallengeTitle from '../../Challenge-Title.jsx';
import TestSuite from '../../Test-Suite.jsx';
import Output from '../../Output.jsx';
-import ToolPanel from './Tool-Panel.jsx';
-import { challengeSelector } from '../../redux/selectors';
import {
openBugModal,
updateHint,
executeChallenge,
- unlockUntrustedCode
-} from '../../redux/actions';
+ unlockUntrustedCode,
+
+ challengeMetaSelector,
+ testsSelector,
+ outputSelector,
+ hintIndexSelector,
+ codeLockedSelector,
+ chatRoomSelector
+} from '../../redux';
+
import { descriptionRegex } from '../../utils';
-import { makeToast } from '../../../../toasts/redux/actions';
+import { challengeSelector } from '../../../../redux';
+import { makeToast } from '../../../../Toasts/redux';
const mapDispatchToProps = {
makeToast,
@@ -30,19 +38,18 @@ const mapDispatchToProps = {
};
const mapStateToProps = createSelector(
challengeSelector,
- state => state.challengesApp.tests,
- state => state.challengesApp.output,
- state => state.challengesApp.hintIndex,
- state => state.challengesApp.isCodeLocked,
- state => state.challengesApp.helpChatRoom,
+ challengeMetaSelector,
+ testsSelector,
+ outputSelector,
+ hintIndexSelector,
+ codeLockedSelector,
+ chatRoomSelector,
(
{
- challenge: {
- description,
- hints = []
- } = {},
- title
+ description,
+ hints = []
},
+ { title },
tests,
output,
hintIndex,
diff --git a/common/app/routes/challenges/views/classic/classic.less b/common/app/routes/challenges/views/classic/classic.less
index de511f9f51..272c1a42ec 100644
--- a/common/app/routes/challenges/views/classic/classic.less
+++ b/common/app/routes/challenges/views/classic/classic.less
@@ -1,21 +1,16 @@
// should match filename and ./ns.json
@ns: classic;
-// make the height no larger than (window - navbar)
-.max-element-height(up-to) {
- max-height: e(%('calc(100vh - %s)', @navbar-total-height));
- overflow-x: hidden;
- overflow-y: auto;
-}
-
-.max-element-height(always) {
- height: e(%('calc(100vh - %s)', @navbar-total-height));
+// challenge panes are bound to the pane size which in turn is
+// bound to the total height minus navbar height
+.max-element-height() {
+ height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.@{ns}-instructions-panel {
- .max-element-height(always);
+ .max-element-height();
padding-bottom: 10px;
padding-left: 5px;
padding-right: 5px;
@@ -90,56 +85,17 @@
}
.@{ns}-editor {
- .max-element-height(always);
+ .max-element-height();
width: 100%;
}
.@{ns}-preview {
- .max-element-height(always);
+ .max-element-height();
width: 100%;
}
.@{ns}-preview-frame {
- border: 1px solid gray;
- border-radius: 5px;
- color: @gray-lighter;
- height: 99%;
- overflow: hidden;
+ .max-element-height();
+ border: none;
width: 100%;
}
-
-.@{ns}-success-modal {
- display: flex;
- flex-direction: column;
- justify-content: center;
- height: 50vh;
-
- .modal-header {
- background-color: @brand-primary;
- margin-bottom: 0;
-
- .close {
- color: #eee;
- font-size: 4rem;
- opacity: 0.6;
- transition: all 300ms ease-out;
- margin-top: 0;
- padding-left: 0;
-
- &:hover {
- opacity: 1;
- }
- }
- }
-
- .modal-body {
- padding: 35px;
- display: flex;
- flex-direction: column;
- justify-content: center;
-
- .fa {
- margin-right: 0;
- }
- }
-}
diff --git a/common/app/routes/challenges/views/classic/index.js b/common/app/routes/challenges/views/classic/index.js
index 479ed82803..f8a8115a06 100644
--- a/common/app/routes/challenges/views/classic/index.js
+++ b/common/app/routes/challenges/views/classic/index.js
@@ -1 +1 @@
-export default from './Classic.jsx';
+export { default, panesMap } from './Show.jsx';
diff --git a/common/app/routes/challenges/views/map/Header.jsx b/common/app/routes/challenges/views/map/Header.jsx
deleted file mode 100644
index 712189bdda..0000000000
--- a/common/app/routes/challenges/views/map/Header.jsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import React, { PropTypes } from 'react';
-import { connect } from 'react-redux';
-import PureComponent from 'react-pure-render/component';
-import { InputGroup, FormControl, Button, Row } from 'react-bootstrap';
-import classnames from 'classnames';
-import {
- clearFilter,
- updateFilter,
- collapseAll,
- expandAll
-} from '../../redux/actions';
-
-const ESC = 27;
-const clearIcon =
;
-const searchIcon =
;
-const bindableActions = {
- clearFilter,
- updateFilter,
- collapseAll,
- expandAll
-};
-const mapStateToProps = state => ({
- isAllCollapsed: state.challengesApp.mapUi.isAllCollapsed,
- filter: state.challengesApp.filter
-});
-const propTypes = {
- clearFilter: PropTypes.func,
- collapseAll: PropTypes.func,
- expandAll: PropTypes.func,
- filter: PropTypes.string,
- isAllCollapsed: PropTypes.bool,
- updateFilter: PropTypes.func
-};
-
-export class Header extends PureComponent {
- constructor(...props) {
- super(...props);
- this.handleKeyDown = this.handleKeyDown.bind(this);
- this.handleClearButton = this.handleClearButton.bind(this);
- }
-
- handleKeyDown(e) {
- if (e.keyCode === ESC) {
- e.preventDefault();
- this.props.clearFilter();
- }
- }
-
- handleClearButton(e) {
- e.preventDefault();
- this.props.clearFilter();
- }
-
- renderSearchAddon(filter) {
- if (!filter) {
- return searchIcon;
- }
- return
{ clearIcon };
- }
-
- render() {
- const {
- filter,
- updateFilter,
- collapseAll,
- expandAll,
- isAllCollapsed
- } = this.props;
- const inputClass = classnames({
- 'map-filter': true,
- filled: !!filter
- });
- const buttonClass = classnames({
- 'center-block': true,
- active: isAllCollapsed
- });
- const buttonCopy = isAllCollapsed ?
- 'Expand all challenges' :
- 'Hide all challenges';
- return (
-
-
-
Challenges required for certifications are marked with a *
-
-
-
-
-
-
-
- { this.renderSearchAddon(filter) }
-
-
-
-
-
-
- );
- }
-}
-
-Header.displayName = 'MapHeader';
-Header.propTypes = propTypes;
-
-export default connect(mapStateToProps, bindableActions)(Header);
diff --git a/common/app/routes/challenges/views/map/Map.jsx b/common/app/routes/challenges/views/map/Map.jsx
deleted file mode 100644
index e40701e45c..0000000000
--- a/common/app/routes/challenges/views/map/Map.jsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { PropTypes } from 'react';
-import { compose } from 'redux';
-import { contain } from 'redux-epic';
-import { connect } from 'react-redux';
-import PureComponent from 'react-pure-render/component';
-import { Col, Row } from 'react-bootstrap';
-
-import MapHeader from './Header.jsx';
-import SuperBlock from './Super-Block.jsx';
-import { fetchChallenges } from '../../redux/actions';
-import { updateTitle } from '../../../../redux/actions';
-
-const mapStateToProps = state => ({
- superBlocks: state.challengesApp.superBlocks
-});
-const mapDispatchToProps = { fetchChallenges, updateTitle };
-const fetchOptions = {
- fetchAction: 'fetchChallenges',
- isPrimed({ superBlocks }) {
- return Array.isArray(superBlocks) && superBlocks.length > 1;
- }
-};
-const propTypes = {
- fetchChallenges: PropTypes.func.isRequired,
- params: PropTypes.object,
- superBlocks: PropTypes.array,
- updateTitle: PropTypes.func.isRequired
-};
-
-export class ShowMap extends PureComponent {
- componentWillMount() {
- // if no params then map is open in drawer
- // do not update title
- if (!this.props.params) {
- return;
- }
- this.props.updateTitle(
- 'A Map to Learn to Code and Become a Software Engineer'
- );
- }
-
- renderSuperBlocks(superBlocks) {
- if (!Array.isArray(superBlocks) || !superBlocks.length) {
- return
No Super Blocks
;
- }
- return superBlocks.map(dashedName => (
-
- ));
- }
-
- render() {
- const { superBlocks } = this.props;
- return (
-
-
-
-
- { this.renderSuperBlocks(superBlocks) }
-
-
-
-
- );
- }
-}
-
-ShowMap.displayName = 'Map';
-ShowMap.propTypes = propTypes;
-
-export default compose(
- connect(mapStateToProps, mapDispatchToProps),
- contain(fetchOptions)
-)(ShowMap);
diff --git a/common/app/routes/challenges/views/project/Forms.jsx b/common/app/routes/challenges/views/project/Forms.jsx
index f0505bfe34..5138fdccaa 100644
--- a/common/app/routes/challenges/views/project/Forms.jsx
+++ b/common/app/routes/challenges/views/project/Forms.jsx
@@ -6,14 +6,15 @@ import {
FormControl
} from 'react-bootstrap';
+import { showProjectSubmit } from './redux';
import SolutionInput from '../../Solution-Input.jsx';
+import { submitChallenge } from '../../redux';
import {
isValidURL,
makeRequired,
createFormValidator,
getValidationState
} from '../../../../utils/form';
-import { submitChallenge, showProjectSubmit } from '../../redux/actions';
const propTypes = {
fields: PropTypes.object,
diff --git a/common/app/routes/challenges/views/project/Project.jsx b/common/app/routes/challenges/views/project/Project.jsx
index e37071ec11..1278903c6c 100644
--- a/common/app/routes/challenges/views/project/Project.jsx
+++ b/common/app/routes/challenges/views/project/Project.jsx
@@ -2,25 +2,25 @@ import React, { PropTypes } from 'react';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import PureComponent from 'react-pure-render/component';
-import { Col, Row, Image } from 'react-bootstrap';
+import { Col, Image } from 'react-bootstrap';
import SidePanel from './Side-Panel.jsx';
import ToolPanel from './Tool-Panel.jsx';
import BugModal from '../../Bug-Modal.jsx';
-import { challengeSelector } from '../../redux/selectors';
+import { challengeMetaSelector } from '../../redux';
+import { challengeSelector } from '../../../../redux';
const mapStateToProps = createSelector(
challengeSelector,
+ challengeMetaSelector,
(
{
- challenge: {
- id,
- description,
- image
- } = {},
- title
- }
+ id,
+ description,
+ image
+ },
+ { title }
) => ({
id,
image,
@@ -47,29 +47,25 @@ export class Project extends PureComponent {
} = this.props;
const imageURL = '//i.imgur.com/' + image + '.png';
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
}
diff --git a/common/app/routes/challenges/views/project/Show.jsx b/common/app/routes/challenges/views/project/Show.jsx
new file mode 100644
index 0000000000..79fc4c204d
--- /dev/null
+++ b/common/app/routes/challenges/views/project/Show.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+import Main from './Project.jsx';
+import { types } from '../../redux';
+import _Map from '../../../../Map';
+import ChildContainer from '../../../../Child-Container.jsx';
+import Panes from '../../../../Panes';
+
+const propTypes = {};
+
+export const panesMap = {
+ [types.toggleMap]: 'Map',
+ [types.toggleMain]: 'Main'
+};
+
+const nameToComponent = {
+ Map: {
+ Component: _Map
+ },
+ Main: {
+ Component: Main
+ }
+};
+
+export default function ShowProject() {
+ return (
+
+
+
+ );
+}
+
+ShowProject.displayName = 'ShowProject';
+ShowProject.propTypes = propTypes;
diff --git a/common/app/routes/challenges/views/project/Tool-Panel.jsx b/common/app/routes/challenges/views/project/Tool-Panel.jsx
index cf0c941a15..68b25fb556 100644
--- a/common/app/routes/challenges/views/project/Tool-Panel.jsx
+++ b/common/app/routes/challenges/views/project/Tool-Panel.jsx
@@ -9,8 +9,19 @@ import {
BackEndForm
} from './Forms.jsx';
-import { submitChallenge, openBugModal } from '../../redux/actions';
-import { challengeSelector } from '../../redux/selectors';
+import { submittingSelector } from './redux';
+
+import {
+ submitChallenge,
+ openBugModal,
+
+ chatRoomSelector
+} from '../../redux';
+
+import {
+ signInLoadingSelector,
+ challengeSelector
+} from '../../../../redux';
import {
simpleProject,
frontEndProject
@@ -31,16 +42,16 @@ const mapDispatchToProps = {
};
const mapStateToProps = createSelector(
challengeSelector,
- state => state.app.isSignedIn,
- state => state.challengesApp.isSubmitting,
- state => state.challengesApp.helpChatRoom,
+ signInLoadingSelector,
+ submittingSelector,
+ chatRoomSelector,
(
- { challenge: { challengeType = simpleProject } },
- isSignedIn,
+ { challengeType = simpleProject },
+ showLoading,
isSubmitting,
helpChatRoom,
) => ({
- isSignedIn,
+ isSignedIn: !showLoading,
isSubmitting,
helpChatRoom,
isSimple: challengeType === simpleProject,
diff --git a/common/app/routes/challenges/views/project/index.js b/common/app/routes/challenges/views/project/index.js
index 6784471995..f8a8115a06 100644
--- a/common/app/routes/challenges/views/project/index.js
+++ b/common/app/routes/challenges/views/project/index.js
@@ -1 +1 @@
-export default from './Project.jsx';
+export { default, panesMap } from './Show.jsx';
diff --git a/common/app/routes/challenges/views/project/ns.json b/common/app/routes/challenges/views/project/ns.json
new file mode 100644
index 0000000000..a8138abf72
--- /dev/null
+++ b/common/app/routes/challenges/views/project/ns.json
@@ -0,0 +1 @@
+"project"
diff --git a/common/app/routes/challenges/views/project/redux/index.js b/common/app/routes/challenges/views/project/redux/index.js
new file mode 100644
index 0000000000..67bde420a6
--- /dev/null
+++ b/common/app/routes/challenges/views/project/redux/index.js
@@ -0,0 +1,27 @@
+import { createTypes } from 'redux-create-types';
+import { createAction, handleActions } from 'redux-actions';
+import ns from '../ns.json';
+
+export const types = createTypes([
+ 'showProjectSubmit'
+], ns);
+
+export const showProjectSubmit = createAction(types.showProjectSubmit);
+
+const initialState = {
+ // project is ready to submit
+ isSubmitting: false
+};
+export const submittingSelector = state => state[ns].isSubmitting;
+
+export default function createReducer() {
+ const reducer = handleActions({
+ [types.showProjectSubmit]: state => ({
+ ...state,
+ isSubmitting: true
+ })
+ }, initialState);
+
+ reducer.toString = () => ns;
+ return [ reducer ];
+}
diff --git a/common/app/routes/challenges/redux/project-normalizer.js b/common/app/routes/challenges/views/project/redux/project-normalizer.js
similarity index 73%
rename from common/app/routes/challenges/redux/project-normalizer.js
rename to common/app/routes/challenges/views/project/redux/project-normalizer.js
index a7c69981dc..4cd8b1ae31 100644
--- a/common/app/routes/challenges/redux/project-normalizer.js
+++ b/common/app/routes/challenges/views/project/redux/project-normalizer.js
@@ -1,4 +1,4 @@
-import { callIfDefined, formatUrl } from '../../../utils/form';
+import { callIfDefined, formatUrl } from '../../../../../utils/form';
export default {
NewFrontEndProject: {
diff --git a/common/app/routes/challenges/views/step/Show.jsx b/common/app/routes/challenges/views/step/Show.jsx
new file mode 100644
index 0000000000..81425940e2
--- /dev/null
+++ b/common/app/routes/challenges/views/step/Show.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import Step from './Step.jsx';
+import { types } from '../../redux';
+import Panes from '../../../../Panes';
+import _Map from '../../../../Map';
+import ChildContainer from '../../../../Child-Container.jsx';
+
+const propTypes = {};
+export const panesMap = {
+ [types.toggleMap]: 'Map',
+ [types.toggleStep]: 'Step'
+};
+
+const nameToComponent = {
+ Map: {
+ Component: _Map
+ },
+ Step: {
+ Component: Step
+ }
+};
+
+export default function ShowStep() {
+ return (
+
+
+
+ );
+}
+
+ShowStep.displayName = 'ShowStep';
+ShowStep.propTypes = propTypes;
diff --git a/common/app/routes/challenges/views/step/Step.jsx b/common/app/routes/challenges/views/step/Step.jsx
index a507bf800e..44959679f7 100644
--- a/common/app/routes/challenges/views/step/Step.jsx
+++ b/common/app/routes/challenges/views/step/Step.jsx
@@ -1,33 +1,36 @@
-import React, { PropTypes } from 'react';
-import classnames from 'classnames';
+import React, { PropTypes, PureComponent } from 'react';
+import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
+import classnames from 'classnames';
import { createSelector } from 'reselect';
-import PureComponent from 'react-pure-render/component';
import LightBox from 'react-images';
+import { Button, Col, Image, Row } from 'react-bootstrap';
import ns from './ns.json';
import {
closeLightBoxImage,
completeAction,
- openLightBoxImage,
+ clickOnImage,
stepBackward,
stepForward,
- submitChallenge,
- updateUnlockedSteps
-} from '../../redux/actions';
-import { challengeSelector } from '../../redux/selectors';
-import { Button, Col, Image, Row } from 'react-bootstrap';
+ updateUnlockedSteps,
+
+ currentIndexSelector,
+ actionCompletedSelector,
+ previousIndexSelector,
+ lightBoxSelector
+} from './redux';
+import { submitChallenge } from '../../redux';
+import { challengeSelector } from '../../../../redux';
const mapStateToProps = createSelector(
challengeSelector,
- state => state.challengesApp.currentIndex,
- state => state.challengesApp.previousIndex,
- state => state.challengesApp.isActionCompleted,
- state => state.challengesApp.isLightBoxOpen,
+ currentIndexSelector,
+ previousIndexSelector,
+ actionCompletedSelector,
+ lightBoxSelector,
(
- {
- challenge: { description = [] }
- },
+ { description = [] },
currentIndex,
previousIndex,
isActionCompleted,
@@ -43,17 +46,27 @@ const mapStateToProps = createSelector(
})
);
-const dispatchActions = {
- closeLightBoxImage,
- completeAction,
- openLightBoxImage,
- stepBackward,
- stepForward,
- submitChallenge,
- updateUnlockedSteps
-};
+function mapDispatchToProps(dispatch) {
+ const dispatchers = bindActionCreators({
+ closeLightBoxImage,
+ completeAction,
+ stepBackward,
+ stepForward,
+ submitChallenge,
+ updateUnlockedSteps
+ }, dispatch);
+ dispatchers.clickOnImage = e => {
+ if (!(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ return dispatch(clickOnImage());
+ }
+ return null;
+ };
+ return () => dispatchers;
+}
const propTypes = {
+ clickOnImage: PropTypes.func.isRequired,
closeLightBoxImage: PropTypes.func.isRequired,
completeAction: PropTypes.func.isRequired,
currentIndex: PropTypes.number,
@@ -61,7 +74,6 @@ const propTypes = {
isLastStep: PropTypes.bool,
isLightBoxOpen: PropTypes.bool,
numOfSteps: PropTypes.number,
- openLightBoxImage: PropTypes.func.isRequired,
step: PropTypes.array,
stepBackward: PropTypes.func,
stepForward: PropTypes.func,
@@ -71,18 +83,6 @@ const propTypes = {
};
export class StepChallenge extends PureComponent {
- constructor(...args) {
- super(...args);
- this.handleLightBoxOpen = this.handleLightBoxOpen.bind(this);
- }
-
- handleLightBoxOpen(e) {
- if (!(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.props.openLightBoxImage();
- }
- }
-
componentWillMount() {
const { updateUnlockedSteps } = this.props;
updateUnlockedSteps([]);
@@ -167,14 +167,15 @@ export class StepChallenge extends PureComponent {
}
renderStep({
- step,
- currentIndex,
- numOfSteps,
- isActionCompleted,
+ clickOnImage,
completeAction,
+ currentIndex,
+ isActionCompleted,
isLastStep,
- stepForward,
- stepBackward
+ numOfSteps,
+ step,
+ stepBackward,
+ stepForward
}) {
if (!Array.isArray(step)) {
return null;
@@ -184,7 +185,7 @@ export class StepChallenge extends PureComponent {
(
+ return steps.map(([ imgUrl, imgAlt ]) => (
@@ -284,4 +285,7 @@ export class StepChallenge extends PureComponent {
StepChallenge.displayName = 'StepChallenge';
StepChallenge.propTypes = propTypes;
-export default connect(mapStateToProps, dispatchActions)(StepChallenge);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(StepChallenge);
diff --git a/common/app/routes/challenges/views/step/index.js b/common/app/routes/challenges/views/step/index.js
index 5fc4e2aca9..f8a8115a06 100644
--- a/common/app/routes/challenges/views/step/index.js
+++ b/common/app/routes/challenges/views/step/index.js
@@ -1 +1 @@
-export default from './Step.jsx';
+export { default, panesMap } from './Show.jsx';
diff --git a/common/app/routes/challenges/views/step/redux/index.js b/common/app/routes/challenges/views/step/redux/index.js
new file mode 100644
index 0000000000..b744c9d22d
--- /dev/null
+++ b/common/app/routes/challenges/views/step/redux/index.js
@@ -0,0 +1,92 @@
+import { createTypes } from 'redux-create-types';
+import { createAction, handleActions } from 'redux-actions';
+import noop from 'lodash/noop';
+
+import stepChallengeEpic from './step-challenge-epic.js';
+import ns from '../ns.json';
+import { types as challenges } from '../../../redux';
+
+export const epics = [
+ stepChallengeEpic
+];
+
+export const types = createTypes([
+ 'stepForward',
+ 'stepBackward',
+ 'goToStep',
+ 'completeAction',
+ 'clickOnImage',
+ 'closeLightBoxImage',
+ 'updateUnlockedSteps'
+], ns);
+
+export const stepForward = createAction(
+ types.stepForward,
+ noop
+);
+export const stepBackward = createAction(
+ types.stepBackward,
+ noop
+);
+export const goToStep = createAction(
+ types.goToStep,
+ (step, isUnlocked) => ({ step, isUnlocked })
+);
+export const completeAction = createAction(
+ types.completeAction,
+ noop
+);
+export const updateUnlockedSteps = createAction(types.updateUnlockedSteps);
+export const clickOnImage = createAction(types.clickOnImage);
+export const closeLightBoxImage = createAction(types.closeLightBoxImage);
+
+const initialState = {
+ // step index tracing
+ currentIndex: 0,
+ previousIndex: -1,
+ // step action
+ isActionCompleted: false,
+ isLightBoxOpen: false,
+ unlockedSteps: []
+};
+
+export const getNS = state => state[ns];
+export const currentIndexSelector = state => getNS(state).currentIndex;
+export const previousIndexSelector = state => getNS(state).previousIndex;
+export const unlockedStepsSelector = state => getNS(state).unlockedSteps;
+export const lightBoxSelector = state => getNS(state).isLightBoxOpen;
+export const actionCompletedSelector = state => getNS(state).isActionCompleted;
+
+export default function createReducers() {
+ const reducer = handleActions({
+ [challenges.challengeUpdated]: () => {
+ console.log('updating step ui');
+ return initialState;
+ },
+ [types.goToStep]: (state, { payload: { step = 0, isUnlocked }}) => ({
+ ...state,
+ currentIndex: step,
+ previousIndex: state.currentIndex,
+ isActionCompleted: isUnlocked
+ }),
+ [types.completeAction]: state => ({
+ ...state,
+ isActionCompleted: true
+ }),
+ [types.updateUnlockedSteps]: (state, { payload }) => ({
+ ...state,
+ unlockedSteps: payload
+ }),
+ [types.clickOnImage]: state => ({
+ ...state,
+ isLightBoxOpen: true
+ }),
+ [types.closeLightBoxImage]: state => ({
+ ...state,
+ isLightBoxOpen: false
+ })
+ }, initialState);
+
+ reducer.toString = () => ns;
+ return [ reducer ];
+}
diff --git a/common/app/routes/challenges/redux/step-challenge-epic.js b/common/app/routes/challenges/views/step/redux/step-challenge-epic.js
similarity index 62%
rename from common/app/routes/challenges/redux/step-challenge-epic.js
rename to common/app/routes/challenges/views/step/redux/step-challenge-epic.js
index 5d94b720d1..af5cb9e71d 100644
--- a/common/app/routes/challenges/redux/step-challenge-epic.js
+++ b/common/app/routes/challenges/views/step/redux/step-challenge-epic.js
@@ -1,7 +1,14 @@
-import types from './types';
-import { goToStep, submitChallenge, updateUnlockedSteps } from './actions';
-import { challengeSelector } from './selectors';
-import getActionsOfType from '../../../../utils/get-actions-of-type';
+import { ofType } from 'redux-epic';
+import {
+ types,
+ goToStep,
+ updateUnlockedSteps,
+
+ unlockedStepsSelector,
+ currentIndexSelector
+} from './';
+import { submitChallenge } from '../../../redux';
+import { challengeSelector } from '../../../../../redux';
function unlockStep(step, unlockedSteps) {
if (!step) {
@@ -12,17 +19,17 @@ function unlockStep(step, unlockedSteps) {
return updateUnlockedSteps(updatedSteps);
}
-export default function stepChallengeEpic(actions, getState) {
- return getActionsOfType(
- actions,
+export default function stepChallengeEpic(actions, { getState }) {
+ return actions::ofType(
types.stepForward,
types.stepBackward,
types.completeAction
)
.map(({ type }) => {
const state = getState();
- const { challenge: { description = [] } } = challengeSelector(state);
- const { challengesApp: { currentIndex, unlockedSteps } } = state;
+ const { description = [] } = challengeSelector(state);
+ const currentIndex = currentIndexSelector(state);
+ const unlockedSteps = unlockedStepsSelector(state);
const numOfSteps = description.length;
const stepFwd = currentIndex + 1;
const stepBwd = currentIndex - 1;
@@ -40,5 +47,6 @@ export default function stepChallengeEpic(actions, getState) {
return goToStep(stepBwd, !!unlockedSteps[stepBwd]);
}
return null;
- });
+ })
+ .filter(Boolean);
}
diff --git a/common/app/routes/challenges/redux/step-challenge-epic.test.js b/common/app/routes/challenges/views/step/redux/step-challenge-epic.test.js
similarity index 85%
rename from common/app/routes/challenges/redux/step-challenge-epic.test.js
rename to common/app/routes/challenges/views/step/redux/step-challenge-epic.test.js
index cbc9dcc0d8..3aaf04aee2 100644
--- a/common/app/routes/challenges/redux/step-challenge-epic.test.js
+++ b/common/app/routes/challenges/views/step/redux/step-challenge-epic.test.js
@@ -2,13 +2,18 @@ import { Observable, config } from 'rx';
import test from 'tape';
import proxy from 'proxyquire';
import sinon from 'sinon';
-import types from './types';
+
+import ns from '../ns.json';
+// import challenges.redux to get around
+// circular dependency
+import { types as app } from '../../../redux';
+import { types } from './';
config.longStackSupport = true;
const challengeSelectorStub = {};
const stepChallengeEpic = proxy(
'./step-challenge-epic',
- { './selectors': challengeSelectorStub }
+ { '../../../../../redux': challengeSelectorStub }
);
const file = 'common/app/routes/challenges/redux/step-challenge-epic';
@@ -33,7 +38,7 @@ test(file, function(t) {
t.test('steps back', t => {
const actions = Observable.of({ type: types.stepBackward });
const state = {
- challengesApp: {
+ [ns]: {
currentIndex: 1,
unlockedSteps: [ true, undefined ] // eslint-disable-line no-undefined
}
@@ -42,12 +47,10 @@ test(file, function(t) {
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
t.assert(_state === state, 'challenge selector not called with state');
return {
- challenge: {
- description: new Array(2)
- }
+ description: new Array(2)
};
});
- stepChallengeEpic(actions, () => state)
+ stepChallengeEpic(actions, { getState: () => state })
.subscribe(
onNextSpy,
e => {
@@ -73,7 +76,7 @@ test(file, function(t) {
t.test('steps forward', t => {
const actions = Observable.of({ type: types.stepForward });
const state = {
- challengesApp: {
+ [ns]: {
currentIndex: 0,
unlockedSteps: []
}
@@ -82,12 +85,10 @@ test(file, function(t) {
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
t.assert(_state === state, 'challenge selector not called with state');
return {
- challenge: {
- description: new Array(2)
- }
+ description: new Array(2)
};
});
- stepChallengeEpic(actions, () => state)
+ stepChallengeEpic(actions, { getState: () => state })
.subscribe(
onNextSpy,
e => {
@@ -112,17 +113,15 @@ test(file, function(t) {
});
t.test('submits on last step forward', t => {
const actions = Observable.of({ type: types.stepForward });
- const state = { challengesApp: { currentIndex: 1 } };
+ const state = { [ns]: { currentIndex: 1 } };
const onNextSpy = sinon.spy();
challengeSelectorStub.challengeSelector = sinon.spy(_state => {
t.assert(_state === state, 'challenge selector not called with state');
return {
- challenge: {
- description: new Array(2)
- }
+ description: new Array(2)
};
});
- stepChallengeEpic(actions, () => state)
+ stepChallengeEpic(actions, { getState: () => state })
.subscribe(
onNextSpy,
e => {
@@ -135,7 +134,7 @@ test(file, function(t) {
);
t.assert(
onNextSpy.calledWithMatch({
- type: types.submitChallenge
+ type: app.submitChallenge
}),
'Epic did not return the expected action'
);
diff --git a/common/app/routes/challenges/views/video/Lecture.jsx b/common/app/routes/challenges/views/video/Lecture.jsx
deleted file mode 100644
index 388f4d9b5c..0000000000
--- a/common/app/routes/challenges/views/video/Lecture.jsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React, { PropTypes } from 'react';
-import { connect } from 'react-redux';
-import { Button, Col, Row } from 'react-bootstrap';
-import Youtube from 'react-youtube';
-import { createSelector } from 'reselect';
-import debug from 'debug';
-
-import { toggleQuestionView } from '../../redux/actions';
-import { challengeSelector } from '../../redux/selectors';
-
-const log = debug('fcc:videos');
-
-const mapStateToProps = createSelector(
- challengeSelector,
- ({
- challenge: {
- id = 'foo',
- dashedName,
- description,
- challengeSeed: [ videoId ] = [ '1' ]
- }
- }) => ({
- id,
- videoId,
- dashedName,
- description
- })
-);
-
-const embedOpts = {
- width: '853',
- height: '480'
-};
-const propTypes = {
- dashedName: PropTypes.string,
- description: PropTypes.array,
- id: PropTypes.string,
- toggleQuestionView: PropTypes.func,
- videoId: PropTypes.string
-};
-
-export class Lecture extends React.Component {
- shouldComponentUpdate(nextProps) {
- const { props } = this;
- return nextProps.id !== props.id;
- }
-
- handleError: log;
-
- renderTranscript(transcript, dashedName) {
- return transcript.map((line, index) => (
-
- ));
- }
-
- render() {
- const {
- id,
- videoId,
- description = [],
- toggleQuestionView
- } = this.props;
-
- const dashedName = 'foo';
-
- return (
-
-
-
-
-
-
-
-
- { this.renderTranscript(description, dashedName) }
-
-
-
-
-
-
- );
- }
-}
-
-Lecture.displayName = 'Lecture';
-Lecture.propTypes = propTypes;
-
-export default connect(
- mapStateToProps,
- { toggleQuestionView }
-)(Lecture);
diff --git a/common/app/routes/challenges/views/video/Questions.jsx b/common/app/routes/challenges/views/video/Questions.jsx
deleted file mode 100644
index 3062095426..0000000000
--- a/common/app/routes/challenges/views/video/Questions.jsx
+++ /dev/null
@@ -1,192 +0,0 @@
-import React, { PropTypes } from 'react';
-import { spring, Motion } from 'react-motion';
-import { connect } from 'react-redux';
-import { Button, Col, Row } from 'react-bootstrap';
-import { createSelector } from 'reselect';
-
-import {
- answerQuestion,
- moveQuestion,
- releaseQuestion,
- grabQuestion
-} from '../../redux/actions';
-import { challengeSelector } from '../../redux/selectors';
-
-const answerThreshold = 100;
-const springProperties = { stiffness: 120, damping: 10 };
-const actionsToBind = {
- answerQuestion,
- moveQuestion,
- releaseQuestion,
- grabQuestion
-};
-
-const mapStateToProps = createSelector(
- challengeSelector,
- state => state.challengesApp,
- state => state.app.isSignedIn,
- (
- { challenge: { tests = [ ] }},
- {
- currentQuestion = 1,
- mouse = [ 0, 0 ],
- delta = [ 0, 0 ],
- isCorrect = false,
- isPressed = false,
- shouldShakeQuestion = false
- },
- isSignedIn
- ) => ({
- tests,
- currentQuestion,
- isCorrect,
- mouse,
- delta,
- isPressed,
- shouldShakeQuestion,
- isSignedIn
- })
-);
-const propTypes = {
- answerQuestion: PropTypes.func,
- currentQuestion: PropTypes.number,
- delta: PropTypes.array,
- grabQuestion: PropTypes.func,
- isCorrect: PropTypes.bool,
- isPressed: PropTypes.bool,
- isSignedIn: PropTypes.bool,
- mouse: PropTypes.array,
- moveQuestion: PropTypes.func,
- releaseQuestion: PropTypes.func,
- shouldShakeQuestion: PropTypes.bool,
- tests: PropTypes.array
-};
-
-class Question extends React.Component {
- handleMouseUp(e, answer, info) {
- e.stopPropagation();
- if (!this.props.isPressed) {
- return null;
- }
-
- const {
- releaseQuestion,
- answerQuestion
- } = this.props;
-
- releaseQuestion();
- return answerQuestion({
- e,
- answer,
- info,
- threshold: answerThreshold
- });
- }
-
- handleMouseMove(isPressed, { delta, moveQuestion }) {
- if (!isPressed) {
- return null;
- }
- return e => moveQuestion({ e, delta });
- }
-
- onAnswer(answer, userAnswer, info) {
- const { isSignedIn, answerQuestion } = this.props;
- return e => {
- if (e && e.preventDefault) {
- e.preventDefault();
- }
-
- answerQuestion({
- answer,
- userAnswer,
- info,
- isSignedIn
- });
- };
- }
-
- renderQuestion(number, question, answer, shouldShakeQuestion, info) {
- const { grabQuestion, isPressed } = this.props;
- const mouseUp = e => this.handleMouseUp(e, answer, info);
- return ({ x }) => {
- const style = {
- WebkitTransform: `translate3d(${ x }px, 0, 0)`,
- transform: `translate3d(${ x }px, 0, 0)`
- };
- return (
-
- Question { number }
- { question }
-
- );
- };
- }
-
- render() {
- const {
- tests = [],
- mouse: [xPosition],
- currentQuestion,
- shouldShakeQuestion
- } = this.props;
-
- const [ question, answer, info ] = tests[currentQuestion - 1] || [];
- const questionElement = this.renderQuestion(
- currentQuestion,
- question,
- answer,
- shouldShakeQuestion,
- info
- );
-
- return (
- this.handleMouseUp(e, answer, info) }
- xs={ 8 }
- xsOffset={ 2 }
- >
-
-
- { questionElement }
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-Question.displayName = 'Question';
-Question.propTypes = propTypes;
-
-export default connect(mapStateToProps, actionsToBind)(Question);
diff --git a/common/app/routes/challenges/views/video/Video.jsx b/common/app/routes/challenges/views/video/Video.jsx
deleted file mode 100644
index 9c7c9b3203..0000000000
--- a/common/app/routes/challenges/views/video/Video.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React, { PropTypes } from 'react';
-import { connect } from 'react-redux';
-import { Col, Row } from 'react-bootstrap';
-import { createSelector } from 'reselect';
-
-import Lecture from './Lecture.jsx';
-import Questions from './Questions.jsx';
-import { resetUi } from '../../redux/actions';
-import { updateTitle } from '../../../../redux/actions';
-import { challengeSelector } from '../../redux/selectors';
-
-const bindableActions = { resetUi, updateTitle };
-const mapStateToProps = createSelector(
- challengeSelector,
- state => state.challengesApp.shouldShowQuestions,
- ({ title }, shouldShowQuestions) => ({
- title,
- shouldShowQuestions
- })
-);
-const propTypes = {
- params: PropTypes.object,
- resetUi: PropTypes.func,
- shouldShowQuestions: PropTypes.bool,
- title: PropTypes.string,
- updateTitle: PropTypes.func
-};
-
-export class Video extends React.Component {
- componentWillMount() {
- const { updateTitle, title } = this.props;
- updateTitle(title);
- }
-
- componentWillUnmount() {
- this.props.resetUi();
- }
-
- componentWillReceiveProps({ title }) {
- if (this.props.title !== title) {
- this.props.resetUi();
- }
- }
-
- renderBody(showQuestions) {
- if (showQuestions) {
- return ;
- }
- return ;
- }
-
- render() {
- const {
- title,
- shouldShowQuestions
- } = this.props;
- return (
-
-
-
-
-
-
- { this.renderBody(shouldShowQuestions) }
-
-
-
- );
- }
-}
-
-Video.displayName = 'Video';
-Video.propTypes = propTypes;
-
-export default connect(
- mapStateToProps,
- bindableActions
-)(Video);
diff --git a/common/app/routes/challenges/views/video/index.js b/common/app/routes/challenges/views/video/index.js
deleted file mode 100644
index a46de403e7..0000000000
--- a/common/app/routes/challenges/views/video/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default from './Video.jsx';
diff --git a/common/app/routes/index.js b/common/app/routes/index.js
index c006b3b21e..ba2e0f7234 100644
--- a/common/app/routes/index.js
+++ b/common/app/routes/index.js
@@ -1,11 +1,11 @@
-import {
- modernChallengesRoute,
- mapRoute,
- challengesRoute
-} from './challenges';
-import NotFound from '../components/NotFound/index.jsx';
+import challenges from './challenges';
+import map from './map';
+import settings from './settings';
+
+import NotFound from '../NotFound';
import { addLang } from '../utils/lang';
-import settingsRoute from './settings';
+
+export { createPanesMap } from './challenges';
export default function createChildRoute(deps) {
return {
@@ -18,14 +18,10 @@ export default function createChildRoute(deps) {
}
},
childRoutes: [
- challengesRoute(deps),
- modernChallengesRoute(deps),
- mapRoute(deps),
- settingsRoute(deps),
- {
- path: '*',
- component: NotFound
- }
+ ...challenges(deps),
+ ...map(deps),
+ ...settings(deps),
+ { path: '*', component: NotFound }
]
};
}
diff --git a/common/app/routes/map/index.js b/common/app/routes/map/index.js
new file mode 100644
index 0000000000..40326d564b
--- /dev/null
+++ b/common/app/routes/map/index.js
@@ -0,0 +1,8 @@
+import ShowMap from '../../Map';
+
+export default function mapRoute() {
+ return [{
+ path: 'map',
+ component: ShowMap
+ }];
+}
diff --git a/common/app/routes/redux.js b/common/app/routes/redux.js
new file mode 100644
index 0000000000..ecd8b12c00
--- /dev/null
+++ b/common/app/routes/redux.js
@@ -0,0 +1,7 @@
+import createChallengesReducer from './challenges/redux';
+
+export default function createReducers() {
+ return [
+ ...createChallengesReducer()
+ ];
+}
diff --git a/common/app/routes/settings/components/Email-Setting.jsx b/common/app/routes/settings/Email-Setting.jsx
similarity index 100%
rename from common/app/routes/settings/components/Email-Setting.jsx
rename to common/app/routes/settings/Email-Setting.jsx
diff --git a/common/app/routes/settings/components/Job-Settings.jsx b/common/app/routes/settings/Job-Settings.jsx
similarity index 100%
rename from common/app/routes/settings/components/Job-Settings.jsx
rename to common/app/routes/settings/Job-Settings.jsx
diff --git a/common/app/routes/settings/components/Language-Settings.jsx b/common/app/routes/settings/Language-Settings.jsx
similarity index 92%
rename from common/app/routes/settings/components/Language-Settings.jsx
rename to common/app/routes/settings/Language-Settings.jsx
index e68a9c54b3..3938f8b7cb 100644
--- a/common/app/routes/settings/components/Language-Settings.jsx
+++ b/common/app/routes/settings/Language-Settings.jsx
@@ -3,9 +3,9 @@ import { createSelector } from 'reselect';
import { reduxForm } from 'redux-form';
import { FormControl, FormGroup } from 'react-bootstrap';
-import { updateMyLang } from '../redux/actions';
-import { userSelector } from '../../../redux/selectors';
-import langs from '../../../../utils/supported-languages';
+import { updateMyLang } from './redux';
+import { userSelector } from '../../redux';
+import langs from '../../../utils/supported-languages';
const propTypes = {
fields: PropTypes.object,
@@ -15,7 +15,7 @@ const propTypes = {
const mapStateToProps = createSelector(
userSelector,
- ({ user: { languageTag } }) => ({
+ ({ languageTag }) => ({
// send null to prevent redux-form from initialize empty
initialValues: languageTag ? { lang: languageTag } : null
})
diff --git a/common/app/routes/settings/components/Locked-Settings.jsx b/common/app/routes/settings/Locked-Settings.jsx
similarity index 100%
rename from common/app/routes/settings/components/Locked-Settings.jsx
rename to common/app/routes/settings/Locked-Settings.jsx
diff --git a/common/app/routes/settings/components/SettingsSkeleton.jsx b/common/app/routes/settings/Settings-Skeleton.jsx
similarity index 98%
rename from common/app/routes/settings/components/SettingsSkeleton.jsx
rename to common/app/routes/settings/Settings-Skeleton.jsx
index 3224f16f93..f05797a826 100644
--- a/common/app/routes/settings/components/SettingsSkeleton.jsx
+++ b/common/app/routes/settings/Settings-Skeleton.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import { Button, Row, Col } from 'react-bootstrap';
-import ns from '../ns.json';
+import ns from './ns.json';
// actual chars required to give buttons some height
// whitespace alone is no good
diff --git a/common/app/routes/settings/components/Settings.jsx b/common/app/routes/settings/Settings.jsx
similarity index 91%
rename from common/app/routes/settings/components/Settings.jsx
rename to common/app/routes/settings/Settings.jsx
index aa8a2f9204..31e224ea0c 100644
--- a/common/app/routes/settings/components/Settings.jsx
+++ b/common/app/routes/settings/Settings.jsx
@@ -10,12 +10,18 @@ import JobSettings from './Job-Settings.jsx';
import SocialSettings from './Social-Settings.jsx';
import EmailSettings from './Email-Setting.jsx';
import LanguageSettings from './Language-Settings.jsx';
-import SettingsSkeleton from './SettingsSkeleton.jsx';
+import SettingsSkeleton from './Settings-Skeleton.jsx';
+import { toggleUserFlag } from './redux';
+import {
+ toggleNightMode,
+ updateTitle,
+
+ signInLoadingSelector,
+ userSelector
+} from '../../redux';
+import ChildContainer from '../../Child-Container.jsx';
-import { toggleUserFlag } from '../redux/actions.js';
-import { userSelector } from '../../../redux/selectors.js';
-import { toggleNightMode, updateTitle } from '../../../redux/actions.js';
const mapDispatchToProps = {
updateTitle,
@@ -29,25 +35,23 @@ const mapDispatchToProps = {
const mapStateToProps = createSelector(
userSelector,
- state => state.app.isSignInAttempted,
+ signInLoadingSelector,
(
{
- user: {
- username,
- email,
- isAvailableForHire,
- isLocked,
- isGithubCool,
- isTwitter,
- isLinkedIn,
- sendMonthlyEmail,
- sendNotificationEmail,
- sendQuincyEmail
- }
+ username,
+ email,
+ isAvailableForHire,
+ isLocked,
+ isGithubCool,
+ isTwitter,
+ isLinkedIn,
+ sendMonthlyEmail,
+ sendNotificationEmail,
+ sendQuincyEmail
},
- isSignInAttempted
+ showLoading,
) => ({
- showLoading: isSignInAttempted,
+ showLoading,
username,
email,
isAvailableForHire,
@@ -127,18 +131,13 @@ export class Settings extends React.Component {
}
if (children) {
return (
-
-
- { children }
-
-
+
+ { children }
+
);
}
return (
-
+
-
+
);
}
}
diff --git a/common/app/routes/settings/components/Social-Settings.jsx b/common/app/routes/settings/Social-Settings.jsx
similarity index 100%
rename from common/app/routes/settings/components/Social-Settings.jsx
rename to common/app/routes/settings/Social-Settings.jsx
diff --git a/common/app/routes/settings/index.js b/common/app/routes/settings/index.js
index 839ba9a0d3..366f1de25c 100644
--- a/common/app/routes/settings/index.js
+++ b/common/app/routes/settings/index.js
@@ -1,12 +1,10 @@
-import Settings from './components/Settings.jsx';
+import Settings from './Settings.jsx';
import updateEmailRoute from './routes/update-email';
export default function settingsRoute(deps) {
- return {
+ return [{
path: 'settings',
component: Settings,
- childRoutes: [
- updateEmailRoute(deps)
- ]
- };
+ childRoutes: updateEmailRoute(deps)
+ }];
}
diff --git a/common/app/routes/settings/redux/actions.js b/common/app/routes/settings/redux/actions.js
deleted file mode 100644
index ae71a60289..0000000000
--- a/common/app/routes/settings/redux/actions.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { createAction } from 'redux-actions';
-
-import createTypes from '../../../utils/create-types';
-
-export const types = createTypes([
- 'toggleUserFlag',
- 'updateMyEmail',
- 'updateMyLang'
-], 'settings');
-
-export const toggleUserFlag = createAction(types.toggleUserFlag);
-export const updateMyEmail = createAction(types.updateMyEmail);
-export const updateMyLang = createAction(
- types.updateMyLang,
- (values) => values.lang
-);
diff --git a/common/app/routes/settings/redux/index.js b/common/app/routes/settings/redux/index.js
index 6e070b31de..085cca21ed 100644
--- a/common/app/routes/settings/redux/index.js
+++ b/common/app/routes/settings/redux/index.js
@@ -1,8 +1,21 @@
-import userUpdateSaga from './update-user-saga';
+import { createTypes } from 'redux-create-types';
+import { createAction } from 'redux-actions';
-export { types } from './actions';
-export * as actions from './actions';
+import userUpdateEpic from './update-user-epic.js';
-export const sagas = [
- userUpdateSaga
+export const epics = [
+ userUpdateEpic
];
+
+export const types = createTypes([
+ 'toggleUserFlag',
+ 'updateMyEmail',
+ 'updateMyLang'
+], 'settings');
+
+export const toggleUserFlag = createAction(types.toggleUserFlag);
+export const updateMyEmail = createAction(types.updateMyEmail);
+export const updateMyLang = createAction(
+ types.updateMyLang,
+ (values) => values.lang
+);
diff --git a/common/app/routes/settings/redux/selectors.js b/common/app/routes/settings/redux/selectors.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/common/app/routes/settings/redux/update-user-saga.js b/common/app/routes/settings/redux/update-user-epic.js
similarity index 69%
rename from common/app/routes/settings/redux/update-user-saga.js
rename to common/app/routes/settings/redux/update-user-epic.js
index 8d250aa609..69c01b5799 100644
--- a/common/app/routes/settings/redux/update-user-saga.js
+++ b/common/app/routes/settings/redux/update-user-epic.js
@@ -1,19 +1,23 @@
import { Observable } from 'rx';
+import { combineEpics, ofType } from 'redux-epic';
import { push } from 'react-router-redux';
-import { types } from './actions';
-import { makeToast } from '../../../toasts/redux/actions';
-import { fetchChallenges } from '../../challenges/redux/actions';
+import { types } from './';
+import { makeToast } from '../../../Toasts/redux';
+import {
+ fetchChallenges,
+ doActionOnError,
+
+ userSelector
+} from '../../../redux';
import {
updateUserFlag,
updateUserEmail,
- updateUserLang,
- doActionOnError
-} from '../../../redux/actions';
-import { userSelector } from '../../../redux/selectors';
+ updateUserLang
+} from '../../../entities';
+
import { postJSON$ } from '../../../../utils/ajax-stream';
import langs from '../../../../utils/supported-languages';
-import combineSagas from '../../../../utils/combine-sagas';
const urlMap = {
isLocked: 'lockdown',
@@ -23,9 +27,8 @@ const urlMap = {
sendMonthlyEmail: 'announcement-email'
};
-export function updateUserEmailSaga(actions$, getState) {
- return actions$
- .filter(({ type }) => type === types.updateMyEmail)
+export function updateUserEmailEpic(actions, { getState }) {
+ return actions::ofType(types.updateMyEmail)
.flatMap(({ payload: email }) => {
const {
app: { user: username, csrfToken: _csrf },
@@ -33,30 +36,30 @@ export function updateUserEmailSaga(actions$, getState) {
} = getState();
const { email: oldEmail } = userMap[username] || {};
const body = { _csrf, email };
- const optimisticUpdate$ = Observable.just(
+ const optimisticUpdate = Observable.just(
updateUserEmail(username, email)
);
- const ajaxUpdate$ = postJSON$('/update-my-email', body)
+ const ajaxUpdate = postJSON$('/update-my-email', body)
.map(({ message }) => makeToast({ message }))
.catch(doActionOnError(() => oldEmail ?
updateUserFlag(username, oldEmail) :
null
));
- return Observable.merge(optimisticUpdate$, ajaxUpdate$);
+ return Observable.merge(optimisticUpdate, ajaxUpdate);
});
}
-export function updateUserLangSaga(actions$, getState) {
- const updateLang$ = actions$
+export function updateUserLangEpic(actions, { getState }) {
+ const updateLang = actions
.filter(({ type, payload }) => (
type === types.updateMyLang && !!langs[payload]
))
.map(({ payload }) => {
const state = getState();
- const { user: { languageTag } } = userSelector(state);
+ const { languageTag } = userSelector(state);
return { lang: payload, oldLang: languageTag };
});
- const ajaxUpdate$ = updateLang$
+ const ajaxUpdate = updateLang
.debounce(250)
.flatMap(({ lang, oldLang }) => {
const { app: { user: username, csrfToken: _csrf } } = getState();
@@ -76,22 +79,22 @@ export function updateUserLangSaga(actions$, getState) {
return updateUserLang(username, oldLang);
}));
});
- const optimistic$ = updateLang$
+ const optimistic = updateLang
.map(({ lang }) => {
const { app: { user: username } } = getState();
return updateUserLang(username, lang);
});
- return Observable.merge(ajaxUpdate$, optimistic$);
+ return Observable.merge(ajaxUpdate, optimistic);
}
-export function updateUserFlagSaga(actions$, getState) {
- const toggleFlag$ = actions$
+export function updateUserFlagEpic(actions, { getState }) {
+ const toggleFlag = actions
.filter(({ type, payload }) => type === types.toggleUserFlag && payload)
.map(({ payload }) => payload);
- const optimistic$ = toggleFlag$.map(flag => {
+ const optimistic = toggleFlag.map(flag => {
const { app: { user: username } } = getState();
return updateUserFlag(username, flag);
});
- const serverUpdate$ = toggleFlag$
+ const serverUpdate = toggleFlag
.debounce(500)
.flatMap(flag => {
const url = `/toggle-${urlMap[ flag ]}`;
@@ -112,11 +115,11 @@ export function updateUserFlagSaga(actions$, getState) {
return updateUserFlag(username, currentValue);
}));
});
- return Observable.merge(optimistic$, serverUpdate$);
+ return Observable.merge(optimistic, serverUpdate);
}
-export default combineSagas(
- updateUserFlagSaga,
- updateUserEmailSaga,
- updateUserLangSaga
+export default combineEpics(
+ updateUserFlagEpic,
+ updateUserEmailEpic,
+ updateUserLangEpic
);
diff --git a/common/app/routes/settings/routes/update-email/Update-Email.jsx b/common/app/routes/settings/routes/update-email/Update-Email.jsx
index b6c5cf87da..a34997ebb2 100644
--- a/common/app/routes/settings/routes/update-email/Update-Email.jsx
+++ b/common/app/routes/settings/routes/update-email/Update-Email.jsx
@@ -1,10 +1,17 @@
import React, { PropTypes } from 'react';
-import { Button, HelpBlock, FormControl, FormGroup } from 'react-bootstrap';
+import {
+ Button,
+ Col,
+ FormControl,
+ FormGroup,
+ HelpBlock,
+ Row
+} from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { reduxForm } from 'redux-form';
import { isEmail } from 'validator';
import { getValidationState } from '../../../../utils/form';
-import { updateMyEmail } from '../../redux/actions';
+import { updateMyEmail } from '../../redux';
const actions = {
updateMyEmail
@@ -65,68 +72,73 @@ export class UpdateEmail extends React.Component {
'Update my Email' :
'Verify Email';
return (
-
-
Update your email address here:
-
-
+
+
+
+
+
+
+
+
);
}
}
diff --git a/common/app/routes/settings/routes/update-email/index.js b/common/app/routes/settings/routes/update-email/index.js
index 1ab52bf222..b56a6288c1 100644
--- a/common/app/routes/settings/routes/update-email/index.js
+++ b/common/app/routes/settings/routes/update-email/index.js
@@ -1,8 +1,8 @@
import UpdateEmail from './Update-Email.jsx';
export default function updateEmailRoute() {
- return {
+ return [{
path: 'update-email',
component: UpdateEmail
- };
+ }];
}
diff --git a/common/app/sagas.js b/common/app/sagas.js
deleted file mode 100644
index f5e1cbf011..0000000000
--- a/common/app/sagas.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { sagas as appSagas } from './redux';
-import { sagas as challengeSagas } from './routes/challenges/redux';
-import { sagas as settingsSagas } from './routes/settings/redux';
-
-export default [
- ...appSagas,
- ...challengeSagas,
- ...settingsSagas
-];
diff --git a/common/app/toasts/redux/actions.js b/common/app/toasts/redux/actions.js
deleted file mode 100644
index 667a4f7e26..0000000000
--- a/common/app/toasts/redux/actions.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { createAction } from 'redux-actions';
-import types from './types';
-
-let key = 0;
-export const makeToast = createAction(
- types.makeToast,
- ({ timeout, ...rest }) => ({
- ...rest,
- // assign current value of key to new toast
- // and then increment key value
- key: key++,
- dismissAfter: timeout || 6000,
- position: rest.position === 'left' ? 'left' : 'right'
- })
-);
-
-export const removeToast = createAction(
- types.removeToast,
- ({ key }) => key
-);
diff --git a/common/app/toasts/redux/index.js b/common/app/toasts/redux/index.js
deleted file mode 100644
index 3a828d6dcd..0000000000
--- a/common/app/toasts/redux/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as types } from './types';
-export { default as reducer } from './reducer';
-export * as actions from './actions';
diff --git a/common/app/toasts/redux/reducer.js b/common/app/toasts/redux/reducer.js
deleted file mode 100644
index ac1312041c..0000000000
--- a/common/app/toasts/redux/reducer.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { handleActions } from 'redux-actions';
-import types from './types';
-
-const initialState = [];
-export default handleActions({
- [types.makeToast]: (state, { payload: toast }) => [
- ...state,
- toast
- ],
- [types.removeToast]: (state, { payload: key }) => state.filter(
- toast => toast.key !== key
- )
-}, initialState);
diff --git a/common/app/toasts/redux/types.js b/common/app/toasts/redux/types.js
deleted file mode 100644
index 5472d76070..0000000000
--- a/common/app/toasts/redux/types.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import createTypes from '../../utils/create-types';
-
-export default createTypes([
- 'makeToast',
- 'removeToast'
-], 'toast');
diff --git a/common/utils/combine-sagas.js b/common/utils/combine-sagas.js
deleted file mode 100644
index 1788b777fb..0000000000
--- a/common/utils/combine-sagas.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Observable } from 'rx';
-
-export default function combineSagas(...sagas) {
- return (actions$, getState, deps) => {
- return Observable.merge(
- sagas.map(saga => saga(actions$, getState, deps))
- );
- };
-}
diff --git a/common/utils/get-actions-of-type.js b/common/utils/get-actions-of-type.js
deleted file mode 100644
index 2a32d1617c..0000000000
--- a/common/utils/get-actions-of-type.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// redux-observable compatible operator
-export function ofType(...keys) {
- return this.filter(({ type }) => {
- const len = keys.length;
- if (len === 1) {
- return type === keys[0];
- } else {
- for (let i = 0; i < len; i++) {
- if (keys[i] === type) {
- return true;
- }
- }
- }
- return false;
- });
-}
-
-export default function getActionsOfType(actions, ...types) {
- const length = types.length;
- return actions
- .filter(({ type }) => {
- if (length === 1) {
- return type === types[0];
- }
- return types.some(_type => _type === type);
- });
-}
diff --git a/common/utils/get-first-challenge.js b/common/utils/get-first-challenge.js
index 0ce74e8a02..2c33d9c4ea 100644
--- a/common/utils/get-first-challenge.js
+++ b/common/utils/get-first-challenge.js
@@ -8,7 +8,7 @@ export function checkMapData(
superBlock,
challengeIdToName
},
- result
+ result: { superBlocks }
}
) {
if (
@@ -16,11 +16,11 @@ export function checkMapData(
!block ||
!superBlock ||
!challengeIdToName ||
- !result ||
- !result.length
+ !superBlocks ||
+ !superBlocks.length
) {
throw new Error(
- 'entities not found, db may not be properly seeded. Crashing hard'
+ 'entities not found, db may not be properly seeded'
);
}
}
diff --git a/gulpfile.js b/gulpfile.js
index 8fdc688c09..17c7621c86 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -188,7 +188,7 @@ function delRev(dest, manifestName) {
gulp.task('serve', function(cb) {
let called = false;
- nodemon({
+ const monitor = nodemon({
script: paths.server,
ext: '.jsx .js .json',
ignore: paths.serverIgnore,
@@ -210,6 +210,14 @@ gulp.task('serve', function(cb) {
debug('Nodemon will restart due to changes in: ', files);
}
});
+
+ process.once('SIGINT', () => {
+ monitor.once('exit', () => {
+ /* eslint-disable no-process-exit */
+ process.exit(0);
+ /* eslint-enable no-process-exit */
+ });
+ });
});
const syncDepenedents = [
diff --git a/package-lock.json b/package-lock.json
index 3e196ecab5..59f7e602bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=",
"dev": true,
"requires": {
- "acorn": "5.0.3",
+ "acorn": "5.1.1",
"css": "2.2.1",
"normalize-path": "2.1.1",
"source-map": "0.5.6",
@@ -18,9 +18,9 @@
},
"dependencies": {
"acorn": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz",
- "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz",
+ "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==",
"dev": true
},
"css": {
@@ -59,25 +59,25 @@
}
},
"@types/bluebird": {
- "version": "3.0.37",
- "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.0.37.tgz",
- "integrity": "sha1-LnazlKqb6kDQQkGjHAiHomAoM4g="
+ "version": "3.5.8",
+ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.8.tgz",
+ "integrity": "sha512-rBfrD56OxaqVjghtVqp2EEX0ieHkRk6IefDVrQXIVGvlhDOEBTvZff4Q02uo84ukVkH4k5eB1cPKGDM2NlFL8A=="
},
"@types/express": {
"version": "4.0.36",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.36.tgz",
"integrity": "sha512-bT9q2eqH/E72AGBQKT50dh6AXzheTqigGZ1GwDiwmx7vfHff0bZOrvUWjvGpNWPNkRmX1vDF6wonG6rlpBHb1A==",
"requires": {
- "@types/express-serve-static-core": "4.0.48",
+ "@types/express-serve-static-core": "4.0.49",
"@types/serve-static": "1.7.31"
}
},
"@types/express-serve-static-core": {
- "version": "4.0.48",
- "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.48.tgz",
- "integrity": "sha512-+W+fHO/hUI6JX36H8FlgdMHU3Dk4a/Fn08fW5qdd7MjPP/wJlzq9fkCrgaH0gES8vohVeqwefHwPa4ylVKyYIg==",
+ "version": "4.0.49",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.49.tgz",
+ "integrity": "sha512-b7mVHoURu1xaP/V6xw1sYwyv9V0EZ7euyi+sdnbnTZxEkAh4/hzPsI6Eflq+ZzHQ/Tgl7l16Jz+0oz8F46MLnA==",
"requires": {
- "@types/node": "8.0.7"
+ "@types/node": "6.0.81"
}
},
"@types/mime": {
@@ -86,16 +86,16 @@
"integrity": "sha512-rek8twk9C58gHYqIrUlJsx8NQMhlxqHzln9Z9ODqiNgv3/s+ZwIrfr+djqzsnVM12xe9hL98iJ20lj2RvCBv6A=="
},
"@types/node": {
- "version": "8.0.7",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.7.tgz",
- "integrity": "sha512-fuCPLPe4yY0nv6Z1rTLFCEC452jl0k7i3gF/c8hdEKpYtEpt6Sk67hTGbxx8C0wmifFGPvKYd/O8CvS6dpgxMQ=="
+ "version": "6.0.81",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.81.tgz",
+ "integrity": "sha512-KdtXOH8l9O2wwOOX+swjbFx+YW/RJFfI14o6S50+Zy79FK1WFGkzFdDsiuNjrG5L6FaBSKpKzSpWgTvXurbbYg=="
},
"@types/serve-static": {
"version": "1.7.31",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.7.31.tgz",
"integrity": "sha1-FUVt6NmNa0z/Mb5savdJKuY/Uho=",
"requires": {
- "@types/express-serve-static-core": "4.0.48",
+ "@types/express-serve-static-core": "4.0.49",
"@types/mime": "1.3.1"
}
},
@@ -196,12 +196,6 @@
"json-stable-stringify": "1.0.1"
}
},
- "ajv-keywords": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz",
- "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=",
- "dev": true
- },
"align-text": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
@@ -258,14 +252,14 @@
"resolved": "https://registry.npmjs.org/aphrodite/-/aphrodite-0.5.0.tgz",
"integrity": "sha1-pLmokCZiOV0nAucKx6K0ymbyVwM=",
"requires": {
- "asap": "2.0.5",
+ "asap": "2.0.6",
"inline-style-prefixer": "2.0.5"
},
"dependencies": {
"asap": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz",
- "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8="
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
}
}
},
@@ -288,13 +282,13 @@
"integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
"dev": true,
"requires": {
- "arr-flatten": "1.0.3"
+ "arr-flatten": "1.1.0"
}
},
"arr-flatten": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.3.tgz",
- "integrity": "sha1-onTthawIhJtr14R8RYB0XcUa37E=",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
"dev": true
},
"array-differ": {
@@ -443,12 +437,13 @@
"dev": true
},
"aws-sdk": {
- "version": "2.80.0",
- "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.80.0.tgz",
- "integrity": "sha1-Yc7XR+uYFglIOuxT6NZU08ydFDU=",
+ "version": "2.83.0",
+ "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.83.0.tgz",
+ "integrity": "sha1-8BBxg1dtLmCTY2q4nM2bVGpTG+c=",
"requires": {
"buffer": "4.9.1",
"crypto-browserify": "1.0.9",
+ "events": "1.1.1",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
@@ -491,7 +486,7 @@
"babel-register": "6.24.1",
"babel-runtime": "6.23.0",
"chokidar": "1.7.0",
- "commander": "2.10.0",
+ "commander": "2.11.0",
"convert-source-map": "1.5.0",
"fs-readdir-recursive": "1.0.0",
"glob": "7.1.2",
@@ -504,13 +499,10 @@
},
"dependencies": {
"commander": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz",
- "integrity": "sha512-q/r9trjmuikWDRJNTBHAVnWhuU6w+z80KgBq7j9YDclik5E7X4xi0KnlZBNFA1zOQ+SH/vHMWd2mC9QTOz7GpA==",
- "dev": true,
- "requires": {
- "graceful-readlink": "1.0.1"
- }
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
}
}
},
@@ -749,7 +741,7 @@
"escodegen": "1.8.1",
"esprima": "2.7.3",
"handlebars": "4.0.10",
- "js-yaml": "3.8.4",
+ "js-yaml": "3.9.0",
"mkdirp": "0.5.1",
"multi-glob": "1.0.1",
"nopt": "3.0.6",
@@ -2070,9 +2062,9 @@
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"caniuse-db": {
- "version": "1.0.30000696",
- "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000696.tgz",
- "integrity": "sha1-5x9cYeH5bHo69OeRrF21XhFzdgQ="
+ "version": "1.0.30000700",
+ "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000700.tgz",
+ "integrity": "sha1-l8/Eg4Ze6oV33Ho2dJKbmr9VMJU="
},
"canonical-json": {
"version": "0.0.4",
@@ -2408,7 +2400,7 @@
"integrity": "sha1-NIxhrpzb4O3+BT2R/0zFIdeQ7eg=",
"requires": {
"for-own": "1.0.0",
- "is-plain-object": "2.0.3",
+ "is-plain-object": "2.0.4",
"kind-of": "3.2.2",
"shallow-clone": "0.1.2"
}
@@ -2617,35 +2609,23 @@
}
},
"compression": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.6.2.tgz",
- "integrity": "sha1-zOsSHsydCcUtetDDNQ6pPd1AK8M=",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.0.tgz",
+ "integrity": "sha1-AwyfGY8WQ6BX13anOOki2kNzAS0=",
"requires": {
"accepts": "1.3.3",
- "bytes": "2.3.0",
+ "bytes": "2.5.0",
"compressible": "2.0.10",
- "debug": "2.2.0",
+ "debug": "2.6.8",
"on-headers": "1.0.1",
+ "safe-buffer": "5.1.1",
"vary": "1.1.1"
},
"dependencies": {
"bytes": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz",
- "integrity": "sha1-1baAoWW2IBc5rLYRVCqrwtjOsHA="
- },
- "debug": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
- "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
- "requires": {
- "ms": "0.7.1"
- }
- },
- "ms": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
- "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.5.0.tgz",
+ "integrity": "sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo="
}
}
},
@@ -2734,7 +2714,7 @@
"integrity": "sha1-fL9Y3/8mdg5eAOAX0KhbS8kLnTc=",
"requires": {
"bluebird": "3.5.0",
- "mongodb": "2.2.29"
+ "mongodb": "2.2.30"
}
},
"console-browserify": {
@@ -2798,9 +2778,9 @@
}
},
"conventional-commit-types": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.1.0.tgz",
- "integrity": "sha1-RdhgOGyaLmU37pHYobYb0EEbPQQ=",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz",
+ "integrity": "sha1-XblXOdbCEqy+e29lahG5QLqmiUY=",
"dev": true
},
"convert-source-map": {
@@ -2871,13 +2851,10 @@
"dev": true
},
"commander": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz",
- "integrity": "sha512-q/r9trjmuikWDRJNTBHAVnWhuU6w+z80KgBq7j9YDclik5E7X4xi0KnlZBNFA1zOQ+SH/vHMWd2mC9QTOz7GpA==",
- "dev": true,
- "requires": {
- "graceful-readlink": "1.0.1"
- }
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
},
"esprima": {
"version": "2.7.3",
@@ -2892,7 +2869,7 @@
"dev": true,
"requires": {
"chalk": "1.1.3",
- "commander": "2.10.0",
+ "commander": "2.11.0",
"is-my-json-valid": "2.16.0",
"pinkie-promise": "2.0.1"
}
@@ -3067,7 +3044,7 @@
"integrity": "sha1-K8oElkyJGbI/P9aonvXmAIsxs/g=",
"dev": true,
"requires": {
- "conventional-commit-types": "2.1.0",
+ "conventional-commit-types": "2.2.0",
"lodash.map": "4.6.0",
"longest": "1.0.1",
"pad-right": "0.2.2",
@@ -3100,7 +3077,7 @@
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"requires": {
- "es5-ext": "0.10.23"
+ "es5-ext": "0.10.24"
}
},
"d3": {
@@ -3538,7 +3515,7 @@
"resolved": "https://registry.npmjs.org/emmet/-/emmet-1.6.3.tgz",
"integrity": "sha1-/hPXdO7jMv5L9sCsjLLWhhbqUME=",
"requires": {
- "caniuse-db": "1.0.30000696"
+ "caniuse-db": "1.0.30000700"
}
},
"emmet-codemirror": {
@@ -3764,9 +3741,9 @@
}
},
"es5-ext": {
- "version": "0.10.23",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.23.tgz",
- "integrity": "sha1-dXi1G+l0IHpUh4IbVlOMIk5Oezg=",
+ "version": "0.10.24",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.24.tgz",
+ "integrity": "sha1-pVh3yZJLwMjZvTwsvhdJWsFwmxQ=",
"requires": {
"es6-iterator": "2.0.1",
"es6-symbol": "3.1.1"
@@ -3778,7 +3755,7 @@
"integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=",
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.23",
+ "es5-ext": "0.10.24",
"es6-symbol": "3.1.1"
}
},
@@ -3788,7 +3765,7 @@
"integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.23",
+ "es5-ext": "0.10.24",
"es6-iterator": "2.0.1",
"es6-set": "0.1.5",
"es6-symbol": "3.1.1",
@@ -3806,7 +3783,7 @@
"integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.23",
+ "es5-ext": "0.10.24",
"es6-iterator": "2.0.1",
"es6-symbol": "3.1.1",
"event-emitter": "0.3.5"
@@ -3818,7 +3795,7 @@
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.23"
+ "es5-ext": "0.10.24"
}
},
"es6-weak-map": {
@@ -3828,7 +3805,7 @@
"dev": true,
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.23",
+ "es5-ext": "0.10.24",
"es6-iterator": "2.0.1",
"es6-symbol": "3.1.1"
}
@@ -3916,7 +3893,7 @@
"inquirer": "0.12.0",
"is-my-json-valid": "2.16.0",
"is-resolvable": "1.0.0",
- "js-yaml": "3.8.4",
+ "js-yaml": "3.9.0",
"json-stable-stringify": "1.0.1",
"levn": "0.3.0",
"lodash": "4.17.4",
@@ -4020,9 +3997,9 @@
}
},
"eslint-plugin-import": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.6.1.tgz",
- "integrity": "sha512-aAMb32eHCQaQmgdb1MOG1hfu/rPiNgGur2IF71VJeDfTXdLpPiKALKWlzxMdcxQOZZ2CmYVKabAxCvjACxH1uQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz",
+ "integrity": "sha512-HGYmpU9f/zJaQiKNQOVfHUh2oLWW3STBrCgH0sHTX1xtsxYlH1zjLh8FlQGEIdZSdTbUMaV36WaZ6ImXkenGxQ==",
"dev": true,
"requires": {
"builtin-modules": "1.1.1",
@@ -4143,14 +4120,14 @@
"integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=",
"dev": true,
"requires": {
- "acorn": "5.0.3",
+ "acorn": "5.1.1",
"acorn-jsx": "3.0.1"
},
"dependencies": {
"acorn": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz",
- "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz",
+ "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==",
"dev": true
}
}
@@ -4200,7 +4177,7 @@
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
"requires": {
"d": "1.0.0",
- "es5-ext": "0.10.23"
+ "es5-ext": "0.10.24"
}
},
"event-stream": {
@@ -4249,8 +4226,7 @@
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
- "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
- "dev": true
+ "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"exit": {
"version": "0.1.2",
@@ -4382,11 +4358,11 @@
}
},
"express-validator": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-3.2.0.tgz",
- "integrity": "sha1-lTer6w9m5Dn54wtO0WxMbCMTGOI=",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-3.2.1.tgz",
+ "integrity": "sha1-RWA+fu5pMYXCGY+969QUkl/9NSQ=",
"requires": {
- "@types/bluebird": "3.0.37",
+ "@types/bluebird": "3.5.8",
"@types/express": "4.0.36",
"bluebird": "3.5.0",
"lodash": "4.17.4",
@@ -4471,9 +4447,9 @@
},
"dependencies": {
"asap": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz",
- "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8="
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
},
"core-js": {
"version": "1.2.7",
@@ -4485,7 +4461,7 @@
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
- "asap": "2.0.5"
+ "asap": "2.0.6"
}
}
}
@@ -4631,20 +4607,6 @@
"requires": {
"findup-sync": "0.4.2",
"merge": "1.2.0"
- },
- "dependencies": {
- "findup-sync": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.2.tgz",
- "integrity": "sha1-qBF9D3MST1pFRoOVef5S1xKfteU=",
- "dev": true,
- "requires": {
- "detect-file": "0.1.0",
- "is-glob": "2.0.1",
- "micromatch": "2.3.11",
- "resolve-dir": "0.1.1"
- }
- }
}
},
"find-parent-dir": {
@@ -4693,9 +4655,9 @@
}
},
"findup-sync": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz",
- "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.2.tgz",
+ "integrity": "sha1-qBF9D3MST1pFRoOVef5S1xKfteU=",
"dev": true,
"requires": {
"detect-file": "0.1.0",
@@ -4711,7 +4673,7 @@
"dev": true,
"requires": {
"expand-tilde": "2.0.2",
- "is-plain-object": "2.0.3",
+ "is-plain-object": "2.0.4",
"object.defaults": "1.1.0",
"object.pick": "1.2.0",
"parse-filepath": "1.0.1"
@@ -4852,7 +4814,7 @@
"dev": true,
"requires": {
"graceful-fs": "4.1.11",
- "jsonfile": "3.0.0",
+ "jsonfile": "3.0.1",
"universalify": "0.1.0"
}
},
@@ -7470,9 +7432,9 @@
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-plain-object": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.3.tgz",
- "integrity": "sha1-wVvz5LZrYtcu+vKSWEhmPsvGGbY=",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"requires": {
"isobject": "3.0.1"
}
@@ -7707,12 +7669,19 @@
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-yaml": {
- "version": "3.8.4",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz",
- "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=",
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.0.tgz",
+ "integrity": "sha512-0LoUNELX4S+iofCT8f4uEHIiRBR+c2AINyC8qRWfC6QNruLtxVZRJaPcu/xwMgFIgDxF25tGHaDjvxzJCNE9yw==",
"requires": {
"argparse": "1.0.9",
- "esprima": "3.1.3"
+ "esprima": "4.0.0"
+ },
+ "dependencies": {
+ "esprima": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
+ "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
+ }
}
},
"js2xmlparser": {
@@ -7794,9 +7763,9 @@
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
},
"jsonfile": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.0.tgz",
- "integrity": "sha1-kufHRE5f/V+jLmqa6LhQNN+DR9A=",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
+ "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
"dev": true,
"requires": {
"graceful-fs": "4.1.11"
@@ -7854,6 +7823,14 @@
"requires": {
"brace-expansion": "1.1.8"
}
+ },
+ "omni-fetch": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/omni-fetch/-/omni-fetch-0.1.0.tgz",
+ "integrity": "sha1-Och1UMG7jdLMH7pUj0L1Jnpa7jk=",
+ "requires": {
+ "caw": "1.2.0"
+ }
}
}
},
@@ -8007,9 +7984,9 @@
},
"dependencies": {
"asap": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz",
- "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true,
"optional": true
},
@@ -8020,7 +7997,7 @@
"dev": true,
"optional": true,
"requires": {
- "asap": "2.0.5"
+ "asap": "2.0.6"
}
}
}
@@ -8062,7 +8039,7 @@
"dev": true,
"requires": {
"extend": "3.0.1",
- "findup-sync": "0.4.3",
+ "findup-sync": "0.4.2",
"fined": "1.1.0",
"flagged-respawn": "0.3.2",
"lodash.isplainobject": "4.0.6",
@@ -8158,13 +8135,10 @@
}
},
"commander": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz",
- "integrity": "sha512-q/r9trjmuikWDRJNTBHAVnWhuU6w+z80KgBq7j9YDclik5E7X4xi0KnlZBNFA1zOQ+SH/vHMWd2mC9QTOz7GpA==",
- "dev": true,
- "requires": {
- "graceful-readlink": "1.0.1"
- }
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
},
"debug": {
"version": "2.2.0",
@@ -8182,7 +8156,7 @@
"dev": true,
"requires": {
"chalk": "1.1.3",
- "commander": "2.10.0",
+ "commander": "2.11.0",
"is-my-json-valid": "2.16.0",
"pinkie-promise": "2.0.1"
}
@@ -8557,6 +8531,11 @@
"lodash.escape": "3.2.0"
}
},
+ "lodash.toarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+ "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
+ },
"lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -8783,7 +8762,7 @@
"resolved": "https://registry.npmjs.org/loopback-connector-remote/-/loopback-connector-remote-1.3.3.tgz",
"integrity": "sha1-ePpyTk4ptNeqXcpVybNKC819Y+A=",
"requires": {
- "loopback-datasource-juggler": "2.54.2",
+ "loopback-datasource-juggler": "2.55.0",
"strong-remoting": "2.33.0"
}
},
@@ -8796,9 +8775,9 @@
}
},
"loopback-datasource-juggler": {
- "version": "2.54.2",
- "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-2.54.2.tgz",
- "integrity": "sha512-c1YDcKalIF3YFR8PekqGej6HtfQylxOAPAXgHLcJjCt9SgfaWGhgOL/vIBTzheH0GbygC4B/DgM4y0RZWxw60A==",
+ "version": "2.55.0",
+ "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-2.55.0.tgz",
+ "integrity": "sha1-eupPKwZW6G1Sufd69uECTW/shXo=",
"requires": {
"async": "1.0.0",
"debug": "2.6.8",
@@ -9259,19 +9238,19 @@
}
},
"mongodb": {
- "version": "2.2.29",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.29.tgz",
- "integrity": "sha512-MrQvIsN6zN80I4hdFo8w46w51cIqD2FJBGsUfApX9GmjXA1aCclEAJbOHaQWjCtabeWq57S3ECzqEKg/9bdBhA==",
+ "version": "2.2.30",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.30.tgz",
+ "integrity": "sha1-jM2AH2dsgXIEDC8rR+lgKg1WNKs=",
"requires": {
"es6-promise": "3.2.1",
- "mongodb-core": "2.1.13",
+ "mongodb-core": "2.1.14",
"readable-stream": "2.2.7"
}
},
"mongodb-core": {
- "version": "2.1.13",
- "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.13.tgz",
- "integrity": "sha512-mbcvqLLZwVcpTrsfBDY3hRNk2SDNJWOvKKxFJSc0pnUBhYojymBc/L0THfQsWwKJrkb2nIXSjfFll1mG/I5OqQ==",
+ "version": "2.1.14",
+ "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.14.tgz",
+ "integrity": "sha1-E8uidkImtb49GJkq8Mljzl6g8P0=",
"requires": {
"bson": "1.0.4",
"require_optional": "1.0.1"
@@ -9524,10 +9503,11 @@
"integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA="
},
"node-emoji": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.5.1.tgz",
- "integrity": "sha1-/ZGOQSdpv4xEgFEjgjOECyr/FqE=",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.7.0.tgz",
+ "integrity": "sha512-dYx345sjhPJUpWaVQKjP0/43y+nTcfBRTZfSciM3ZEbRGaU/9AKaHBPf7AJ9vOKcK0W3v67AgI4m4oo02NLHhQ==",
"requires": {
+ "lodash.toarray": "4.4.0",
"string.prototype.codepointat": "0.2.0"
}
},
@@ -9679,7 +9659,7 @@
"resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.5.1.tgz",
"integrity": "sha1-3AWYwb9T6GUuYy6PMWks4CLX3qk=",
"requires": {
- "aws-sdk": "2.80.0"
+ "aws-sdk": "2.83.0"
}
},
"nodemailer-shared": {
@@ -9951,14 +9931,6 @@
}
}
},
- "omni-fetch": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/omni-fetch/-/omni-fetch-0.1.0.tgz",
- "integrity": "sha1-Och1UMG7jdLMH7pUj0L1Jnpa7jk=",
- "requires": {
- "caw": "1.2.0"
- }
- },
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -9986,9 +9958,9 @@
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
},
"opbeat": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/opbeat/-/opbeat-4.14.0.tgz",
- "integrity": "sha1-rpB3qvqRS3KkSAGQWjK8tT1+dd8=",
+ "version": "4.14.1",
+ "resolved": "https://registry.npmjs.org/opbeat/-/opbeat-4.14.1.tgz",
+ "integrity": "sha512-8kcSCTNIXy5oW9lW+QFOL/r2iOs395JC5uU8BEtDYAGBTfPv+9nbPriUXc41duACtnIRbzW5qK0lG0vrOo28nw==",
"requires": {
"after-all-results": "2.0.0",
"console-log-level": "1.4.0",
@@ -10098,8 +10070,7 @@
"options": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
- "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=",
- "dev": true
+ "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8="
},
"orchestrator": {
"version": "0.3.8",
@@ -10902,6 +10873,35 @@
"react-prop-types": "0.4.0",
"uncontrollable": "4.1.0",
"warning": "3.0.0"
+ },
+ "dependencies": {
+ "react-overlays": {
+ "version": "0.6.12",
+ "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.6.12.tgz",
+ "integrity": "sha1-oHnHUMxCnX20x0dKlbS1QDPiVcM=",
+ "requires": {
+ "classnames": "2.2.5",
+ "dom-helpers": "3.2.1",
+ "react-prop-types": "0.4.0",
+ "warning": "3.0.0"
+ }
+ },
+ "react-prop-types": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/react-prop-types/-/react-prop-types-0.4.0.tgz",
+ "integrity": "sha1-+ZsL+0AGkpya8gUefBQUpcdbk9A=",
+ "requires": {
+ "warning": "3.0.0"
+ }
+ },
+ "uncontrollable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-4.1.0.tgz",
+ "integrity": "sha1-4DWCkSUuGGUiLZCTmxny9J+Bwak=",
+ "requires": {
+ "invariant": "2.2.2"
+ }
+ }
}
},
"react-codemirror": {
@@ -10939,12 +10939,6 @@
"prop-types": "15.5.10"
}
},
- "react-hot-api": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/react-hot-api/-/react-hot-api-0.4.7.tgz",
- "integrity": "sha1-p+IqVtJS4Rq9k2a2EmTPRJLFgXE=",
- "dev": true
- },
"react-hot-loader": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-1.3.1.tgz",
@@ -10955,6 +10949,12 @@
"source-map": "0.4.4"
},
"dependencies": {
+ "react-hot-api": {
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/react-hot-api/-/react-hot-api-0.4.7.tgz",
+ "integrity": "sha1-p+IqVtJS4Rq9k2a2EmTPRJLFgXE=",
+ "dev": true
+ },
"source-map": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
@@ -10973,8 +10973,31 @@
"requires": {
"aphrodite": "0.5.0",
"prop-types": "15.5.10",
- "react-scrolllock": "1.0.6",
+ "react-scrolllock": "1.0.8",
"react-transition-group": "1.2.0"
+ },
+ "dependencies": {
+ "react-scrolllock": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-1.0.8.tgz",
+ "integrity": "sha1-Su6FgWeDJ7lss4pyt6IOJhBmTlo=",
+ "requires": {
+ "create-react-class": "15.6.0",
+ "prop-types": "15.5.10"
+ }
+ },
+ "react-transition-group": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.0.tgz",
+ "integrity": "sha1-tR/JIbDDg1p+98Vxx5/ILHPpIE8=",
+ "requires": {
+ "chain-function": "1.0.0",
+ "dom-helpers": "3.2.1",
+ "loose-envify": "1.3.1",
+ "prop-types": "15.5.10",
+ "warning": "3.0.0"
+ }
+ }
}
},
"react-lazy-cache": {
@@ -11007,25 +11030,6 @@
"react-notification": {
"version": "git+https://github.com/BerkeleyTrue/react-notification.git#0c503b92a92cc1db843e6f8802d6d8b292546b5e"
},
- "react-overlays": {
- "version": "0.6.12",
- "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.6.12.tgz",
- "integrity": "sha1-oHnHUMxCnX20x0dKlbS1QDPiVcM=",
- "requires": {
- "classnames": "2.2.5",
- "dom-helpers": "3.2.1",
- "react-prop-types": "0.4.0",
- "warning": "3.0.0"
- }
- },
- "react-prop-types": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/react-prop-types/-/react-prop-types-0.4.0.tgz",
- "integrity": "sha1-+ZsL+0AGkpya8gUefBQUpcdbk9A=",
- "requires": {
- "warning": "3.0.0"
- }
- },
"react-pure-render": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-pure-render/-/react-pure-render-1.0.2.tgz",
@@ -11071,26 +11075,6 @@
"resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-4.0.8.tgz",
"integrity": "sha1-InQDWWtRUeGCN32rg1tdRfD4BU4="
},
- "react-scrolllock": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-1.0.6.tgz",
- "integrity": "sha1-A2Gaq+xyRZbtyx4iTrJaY83fF2w=",
- "requires": {
- "prop-types": "15.5.10"
- }
- },
- "react-transition-group": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.0.tgz",
- "integrity": "sha1-tR/JIbDDg1p+98Vxx5/ILHPpIE8=",
- "requires": {
- "chain-function": "1.0.0",
- "dom-helpers": "3.2.1",
- "loose-envify": "1.3.1",
- "prop-types": "15.5.10",
- "warning": "3.0.0"
- }
- },
"react-youtube": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.4.0.tgz",
@@ -11218,9 +11202,9 @@
}
},
"redux-actions": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.0.3.tgz",
- "integrity": "sha1-FVCrqd7xeRZszSNNB2chBKc22Ik=",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.2.1.tgz",
+ "integrity": "sha1-1kGGslZJoTwFR4VH1811N7iSQQ0=",
"requires": {
"invariant": "2.2.2",
"lodash": "4.17.4",
@@ -11238,9 +11222,9 @@
}
},
"redux-epic": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/redux-epic/-/redux-epic-0.2.0.tgz",
- "integrity": "sha1-HQJ8ZgG2nJ+oqSfHQdug4O22dJQ=",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/redux-epic/-/redux-epic-0.3.0.tgz",
+ "integrity": "sha1-g/SioK4fowK/QC397KNi1jB9xr8=",
"requires": {
"debug": "2.6.8",
"invariant": "2.2.2",
@@ -11696,6 +11680,14 @@
"resolved": "https://registry.npmjs.org/rx/-/rx-4.0.8.tgz",
"integrity": "sha1-23Lz6ZRiQhatq63uI/1u4K7ApKE="
},
+ "rx-dom": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/rx-dom/-/rx-dom-7.0.3.tgz",
+ "integrity": "sha1-+HbzmEU//DRqxlGH7dbnF+0R/gk=",
+ "requires": {
+ "rx": "4.0.8"
+ }
+ },
"rx-lite": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz",
@@ -12014,12 +12006,12 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"sinon": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.6.tgz",
- "integrity": "sha1-lTeOfg+XapcS6bRZH/WznnPcPd4=",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.7.tgz",
+ "integrity": "sha1-FFFhSi6qsFu02HbBM1zUATLsUSc=",
"dev": true,
"requires": {
- "diff": "3.2.0",
+ "diff": "3.3.0",
"formatio": "1.2.0",
"lolex": "1.6.0",
"native-promise-only": "0.8.1",
@@ -12030,9 +12022,9 @@
},
"dependencies": {
"diff": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
- "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz",
+ "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==",
"dev": true
},
"isarray": {
@@ -12193,7 +12185,7 @@
"requires": {
"debug": "2.6.8",
"es6-promise": "3.2.1",
- "js-yaml": "3.8.4",
+ "js-yaml": "3.9.0",
"lodash.clonedeep": "4.5.0",
"semver": "5.3.0",
"snyk-module": "1.8.1",
@@ -12542,13 +12534,6 @@
"integrity": "sha1-MZJGHfo4x4Qk3Zv46gJWGaElqhA=",
"requires": {
"options": "0.0.6"
- },
- "dependencies": {
- "options": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
- "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8="
- }
}
},
"sshpk": {
@@ -12899,7 +12884,7 @@
"requires": {
"btoa": "1.1.2",
"cookiejar": "2.1.1",
- "js-yaml": "3.8.4",
+ "js-yaml": "3.9.0",
"lodash-compat": "3.10.2",
"q": "1.5.0",
"superagent": "2.3.0"
@@ -12924,6 +12909,12 @@
"string-width": "2.1.0"
},
"dependencies": {
+ "ajv-keywords": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz",
+ "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=",
+ "dev": true
+ },
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
@@ -13416,14 +13407,6 @@
"integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=",
"dev": true
},
- "uncontrollable": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-4.1.0.tgz",
- "integrity": "sha1-4DWCkSUuGGUiLZCTmxny9J+Bwak=",
- "requires": {
- "invariant": "2.2.2"
- }
- },
"undefsafe": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-0.0.3.tgz",
@@ -13630,7 +13613,7 @@
"integrity": "sha1-bVAVMxvxlsIq+4gNPzO87x3q/qY=",
"dev": true,
"requires": {
- "conventional-commit-types": "2.1.0",
+ "conventional-commit-types": "2.2.0",
"find-parent-dir": "0.3.0",
"findup": "0.1.5",
"semver-regex": "1.0.0"
@@ -14002,9 +13985,9 @@
}
},
"webpack-hot-middleware": {
- "version": "2.18.0",
- "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.18.0.tgz",
- "integrity": "sha1-oWu1Nbg6aslKeKxevOTzBZ6CdNM=",
+ "version": "2.18.2",
+ "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.18.2.tgz",
+ "integrity": "sha512-dB7uOnUWsojZIAC6Nwi5v3tuaQNd2i7p4vF5LsJRyoTOgr2fRYQdMKQxRZIZZaz0cTPBX8rvcWU1A6/n7JTITg==",
"dev": true,
"requires": {
"ansi-html": "0.0.7",
@@ -14014,9 +13997,9 @@
}
},
"webpack-manifest-plugin": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-1.1.0.tgz",
- "integrity": "sha1-a2xxiq3oolN5lXhLRr0umDYFfKo=",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-1.1.2.tgz",
+ "integrity": "sha512-IdeSoftCEzeVrkTy4XFsYBLQk7fWiKPUKUvSdb68/alpoKHP9X9MhMcYdkCIdAXclUhBIcyL03wFLr76wZG45A==",
"dev": true,
"requires": {
"fs-extra": "0.30.0",
diff --git a/package.json b/package.json
index ebee9c8fd3..b1ace4c3cb 100644
--- a/package.json
+++ b/package.json
@@ -118,13 +118,14 @@
"react-router-redux": "^4.0.7",
"react-youtube": "^7.0.0",
"redux": "^3.0.5",
- "redux-actions": "^2.0.2",
+ "redux-actions": "^2.0.3",
"redux-create-types": "0.0.1",
- "redux-epic": "^0.2.0",
+ "redux-epic": "^0.3.0",
"redux-form": "^5.2.3",
"request": "^2.65.0",
"reselect": "^3.0.0",
"rx": "~4.0.8",
+ "rx-dom": "^7.0.3",
"sanitize-html": "^1.11.1",
"snyk": "^1.30.1",
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
diff --git a/server/boot/settings.js b/server/boot/settings.js
index 1c4111e33a..5a407b080b 100644
--- a/server/boot/settings.js
+++ b/server/boot/settings.js
@@ -1,5 +1,6 @@
-import { ifNoUser401 } from '../utils/middleware';
import { isMongoId } from 'validator';
+
+import { ifNoUser401 } from '../utils/middleware';
import supportedLanguages from '../../common/utils/supported-languages.js';
export default function settingsController(app) {
diff --git a/server/services/map.js b/server/services/map.js
index ba83430efd..8b534d903a 100644
--- a/server/services/map.js
+++ b/server/services/map.js
@@ -34,45 +34,45 @@ function getFirstChallenge(challengeMap$) {
// this is a hard search
// falls back to soft search
-function getChallengeAndBlock(
+function getChallenge(
challengeDashedName,
blockDashedName,
challengeMap$,
lang
) {
return challengeMap$
- .flatMap(({ entities }) => {
+ .flatMap(({ entities, result: { superBlocks } }) => {
const block = entities.block[blockDashedName];
const challenge = entities.challenge[challengeDashedName];
- if (
- !block ||
- !challenge ||
- !loadComingSoonOrBetaChallenge(challenge)
- ) {
- return getChallengeByDashedName(
- challengeDashedName,
- challengeMap$,
- lang
- );
- }
- return Observable.just({
- redirect: block.dashedName !== blockDashedName ?
- `/challenges/${block.dashedName}/${challenge.dashedName}` :
- false,
- entities: {
- challenge: {
- [challenge.dashedName]: mapChallengeToLang(challenge, lang)
+ return Observable.if(
+ () => (
+ !blockDashedName ||
+ !block ||
+ !challenge ||
+ !loadComingSoonOrBetaChallenge(challenge)
+ ),
+ getChallengeByDashedName(challengeDashedName, challengeMap$),
+ Observable.just(challenge)
+ )
+ .map(challenge => ({
+ redirect: challenge.block !== blockDashedName ?
+ `/challenges/${block.dashedName}/${challenge.dashedName}` :
+ false,
+ entities: {
+ challenge: {
+ [challenge.dashedName]: mapChallengeToLang(challenge, lang)
+ }
+ },
+ result: {
+ block: block.dashedName,
+ challenge: challenge.dashedName,
+ superBlocks
}
- },
- result: {
- block: block.dashedName,
- challenge: challenge.dashedName
- }
- });
+ }));
});
}
-function getChallengeByDashedName(dashedName, challengeMap$, lang) {
+function getChallengeByDashedName(dashedName, challengeMap$) {
const challengeName = unDasherize(dashedName)
.replace(challengesRegex, '');
const testChallengeName = new RegExp(challengeName, 'i');
@@ -94,40 +94,22 @@ function getChallengeByDashedName(dashedName, challengeMap$, lang) {
return Observable.just(challengeOrNull);
}
return getFirstChallenge(challengeMap$);
- })
- .map(challenge => ({
- redirect:
- `/challenges/${challenge.block}/${challenge.dashedName}`,
- entities: {
- challenge: {
- [challenge.dashedName]: mapChallengeToLang(challenge, lang)
- }
- },
- result: {
- challenge: challenge.dashedName,
- block: challenge.block
- }
- }));
+ });
}
export default function mapService(app) {
const Block = app.models.Block;
- const challengeMap$ = cachedMap(Block);
+ const challengeMap = cachedMap(Block);
return {
name: 'map',
read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
log(`${lang} language requested`);
- if (block && dashedName) {
- return getChallengeAndBlock(dashedName, block, challengeMap$, lang)
- .subscribe(challenge => cb(null, challenge), cb);
- }
- if (dashedName) {
- return getChallengeByDashedName(dashedName, challengeMap$, lang)
- .subscribe(challenge => cb(null, challenge), cb);
- }
- return challengeMap$
- .map(getMapForLang(lang))
- .subscribe(map => cb(null, map), cb);
+ return Observable.if(
+ () => !!dashedName,
+ getChallenge(dashedName, block, challengeMap, lang),
+ challengeMap.map(getMapForLang(lang))
+ )
+ .subscribe(results => cb(null, results), cb);
}
};
}
diff --git a/server/utils/map.js b/server/utils/map.js
index e530ff1890..d5a4a3ff78 100644
--- a/server/utils/map.js
+++ b/server/utils/map.js
@@ -21,20 +21,17 @@ const mapSchema = valuesOf(superBlock);
let mapObservableCache;
/*
* interface ChallengeMap {
- * result: [superBlockDashedName: String]
+ * result: {
+ * superBlocks: [ ...superBlockDashedName: String ]
+* },
* entities: {
* superBlock: {
- * [superBlockDashedName: String]: {
- * blocks: [blockDashedName: String]
- * }
+ * [ ...superBlockDashedName: String ]: SuperBlock
* },
* block: {
- * [blockDashedName: String]: {
- * challenges: [challengeDashedName: String]
- * }
- * },
+ * [ ...blockDashedName: String ]: Block,
* challenge: {
- * [challengeDashedName: String]: Challenge
+ * [ ...challengeDashedName: String ]: Challenge
* }
* }
* }
@@ -88,14 +85,16 @@ export function cachedMap(Block) {
})
.map(map => {
// re-order superBlocks result
- const result = Object.keys(map.result).reduce((result, supName) => {
+ const superBlocks = Object.keys(map.result).reduce((result, supName) => {
const index = map.entities.superBlock[supName].order;
result[index] = supName;
return result;
}, []);
return {
...map,
- result
+ result: {
+ superBlocks
+ }
};
})
.shareReplay();
diff --git a/server/views/partials/react-stylesheets.jade b/server/views/partials/react-stylesheets.jade
index 0517a76730..98686bc05d 100644
--- a/server/views/partials/react-stylesheets.jade
+++ b/server/views/partials/react-stylesheets.jade
@@ -1,9 +1,8 @@
link(rel='stylesheet', type='text/css' href='/css/lato.css')
link(rel='stylesheet', type='text/css' href='/css/ubuntu.css')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css')
-link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.15.2/codemirror.min.css')
-link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.15.2/addon/lint/lint.min.css')
-link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.15.2/theme/monokai.min.css')
+link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.28.0/codemirror.min.css')
+link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.28.0/addon/lint/lint.min.css')
link(rel='stylesheet', href=rev('/css', 'main.css'))
include meta