diff --git a/common/app/entities/index.js b/common/app/entities/index.js
index e7dd2943ce..b912001199 100644
--- a/common/app/entities/index.js
+++ b/common/app/entities/index.js
@@ -191,11 +191,11 @@ export default composeReducers(
() => ({
[
combineActions(
- app.fetchChallenges.complete,
+ app.fetchNewBlock.complete,
map.fetchMapUi.complete
)
]: (state, { payload: { entities } }) => merge({}, state, entities),
- [app.fetchChallenges.complete]:
+ [app.fetchNewBlock.complete]:
(state, { payload: { entities: { block }}}) => ({
...state,
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
diff --git a/common/app/helperComponents/OverlayLoader.jsx b/common/app/helperComponents/OverlayLoader.jsx
deleted file mode 100644
index 880ed41ece..0000000000
--- a/common/app/helperComponents/OverlayLoader.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import styles from './overlayLoader-styles';
-
-function LoaderCircle({ delay, origin }, i) {
- return (
-
- );
-}
-
-LoaderCircle.propTypes = {
- delay: PropTypes.string.isRequired,
- origin: PropTypes.string.isRequired
-};
-LoaderCircle.displayName = 'LoaderCircle';
-
-const animationProps = [
- {
- delay: '0.24s',
- origin: '0% 0%'
- },
- {
- delay: '0.95s',
- origin: '0% 100%'
- },
- {
- delay: '0.67s',
- origin: '100% 0%'
- },
- {
- delay: '1.33s',
- origin: '100% 100%'
- }
-];
-
-function OverlayLoader() {
- return (
-
-
-
-
- );
-}
-
-OverlayLoader.displayName = 'OverlayLoader';
-
-export default OverlayLoader;
diff --git a/common/app/helperComponents/SkeletonSprite.jsx b/common/app/helperComponents/SkeletonSprite.jsx
new file mode 100644
index 0000000000..306105b532
--- /dev/null
+++ b/common/app/helperComponents/SkeletonSprite.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import styles from './skeletonStyles';
+
+function SkeletonSprite() {
+ return (
+
+
+
+
+ );
+}
+
+SkeletonSprite.displayName = 'SkeletonSprite';
+
+export default SkeletonSprite;
diff --git a/common/app/helperComponents/index.js b/common/app/helperComponents/index.js
index d3150f4d04..06f1160b3b 100644
--- a/common/app/helperComponents/index.js
+++ b/common/app/helperComponents/index.js
@@ -1,5 +1,5 @@
+export { default as ButtonSpacer } from './ButtonSpacer.jsx';
export { default as FullWidthRow } from './FullWidthRow.jsx';
export { default as Loader } from './Loader.jsx';
-export { default as OverlayLoader } from './OverlayLoader.jsx';
+export { default as SkeletonSprite } from './SkeletonSprite.jsx';
export { default as Spacer } from './Spacer.jsx';
-export { default as ButtonSpacer } from './ButtonSpacer.jsx';
diff --git a/common/app/helperComponents/overlayLoader-styles.js b/common/app/helperComponents/overlayLoader-styles.js
deleted file mode 100644
index c710ed37ea..0000000000
--- a/common/app/helperComponents/overlayLoader-styles.js
+++ /dev/null
@@ -1,74 +0,0 @@
-export default `
-
-.svg-container {
- position: fixed;
- top: 0;
- left: 0;
- z-index:5;
- display: -webkit-box;
- display: -ms-flexbox;
- display: flex;
- height: 100vh;
- -webkit-box-pack: center;
- -ms-flex-pack: center;
- justify-content: center;
- -webkit-box-align:center;
- -ms-flex-align:center;
- align-items:center;
-}
-
-.svg-container + div {
- -webkit-filter: blur(5px);
- filter: blur(5px);
-}
-
-@-webkit-keyframes overlay-loader {
- 0% {
- -webkit-transform: scale(0.1);
- transform: scale(0.1);
- opacity: 0;
- }
- 50% {
- -webkit-transform: scale(0.8);
- transform: scale(0.8);
- opacity: 0.8;
- }
- 70% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- -webkit-transform: scale(1.2);
- transform: scale(1.2);
- }
-}
-
-@keyframes overlay-loader {
- 0% {
- -webkit-transform: scale(0.1);
- transform: scale(0.1);
- opacity: 0;
- }
- 70% {
- opacity: 1;
- }
- 100% {
- opacity: 0.0;
- -webkit-transform: scale(1);
- transform: scale(1);
- }
-}
-
-.innerCircle {
- -webkit-animation-duration: 2s;
- animation-duration: 2s;
- -webkit-animation-iteration-count: infinite;
- animation-iteration-count: infinite;
- -webkit-animation-name: overlay-loader;
- animation-name: overlay-loader;
- -webkit-animation-timing-function: ease-out;
- animation-timing-function: ease-out;
- opacity: 0;
-}
-
-`;
diff --git a/common/app/helperComponents/skeletonStyles.js b/common/app/helperComponents/skeletonStyles.js
new file mode 100644
index 0000000000..bd3a9bead4
--- /dev/null
+++ b/common/app/helperComponents/skeletonStyles.js
@@ -0,0 +1,60 @@
+export default `
+
+.sprite-container {
+ height: 100%;
+ width: 100%;
+}
+
+.sprite-svg {
+ height: 100%;
+ width: 100%;
+ background: #aaa;
+
+}
+
+@-webkit-keyframes shimmer{
+ 0% {
+ -webkit-transform: translateX(0%);
+ transform: translateX(0%);
+ stroke-width: 2px;
+ }
+ 35% {
+ stroke-width: 30px;
+ }
+ 100% {
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+ stroke-width: 2px;
+ }
+}
+
+@keyframes shimmer{
+ 0% {
+ -webkit-transform: translateX(0%);
+ transform: translateX(0%);
+ stroke-width: 2px;
+ }
+ 35% {
+ stroke-width: 30px;
+ }
+ 100% {
+ -webkit-transform: translateX(100%);
+ transform: translateX(100%);
+ stroke-width: 2px;
+ }
+}
+
+.sprite {
+ -webkit-animation-name: shimmer;
+ animation-name: shimmer;
+ width: 2px;
+ -webkit-animation-duration: 2s;
+ animation-duration: 2s;
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+ -webkit-animation-direction: normal;
+ animation-direction: normal;
+}
+`;
diff --git a/common/app/redux/fetch-challenges-epic.js b/common/app/redux/fetch-challenges-epic.js
index 5cace6b1f1..c31c937ce6 100644
--- a/common/app/redux/fetch-challenges-epic.js
+++ b/common/app/redux/fetch-challenges-epic.js
@@ -1,5 +1,6 @@
import { Observable } from 'rx';
import { combineEpics, ofType } from 'redux-epic';
+import _ from 'lodash';
import debug from 'debug';
import {
@@ -9,8 +10,7 @@ import {
delayedRedirect,
fetchChallengeCompleted,
- fetchChallengesCompleted,
- fetchNewBlock,
+ fetchNewBlockComplete,
challengeSelector,
nextChallengeSelector
} from './';
@@ -21,7 +21,7 @@ import {
import { shapeChallenges } from './utils';
import { types as challenge } from '../routes/Challenges/redux';
-import { langSelector } from '../Router/redux';
+import { langSelector, paramsSelector } from '../Router/redux';
const isDev = debug.enabled('fcc:*');
@@ -60,61 +60,59 @@ export function fetchChallengesForBlockEpic(
{ getState },
{ services }
) {
- return actions::ofType(
- types.appMounted,
- types.updateChallenges,
- types.fetchNewBlock.start
- )
- .flatMapLatest(({ type, payload }) => {
- const fetchAnotherBlock = type === types.fetchNewBlock.start;
- const state = getState();
- let {
- block: blockName = 'basic-html-and-html5'
- } = challengeSelector(state);
- const lang = langSelector(state);
- if (fetchAnotherBlock) {
- const fullBlocks = fullBlocksSelector(state);
- if (fullBlocks.includes(payload)) {
- return Observable.of(null);
- }
- blockName = payload;
- }
+ const onAppMount = actions::ofType(types.appMounted)
+ .map(() => {
+ const {
+ block = 'basic-html-and-html5'
+ } = challengeSelector(getState());
+ return block;
+ });
+ const onNewChallenge = actions::ofType(challenge.moveToNextChallenge)
+ .map(() => {
+ const {
+ isNewBlock,
+ isNewSuperBlock,
+ nextChallenge
+ } = nextChallengeSelector(getState());
+ const isNewBlockRequired = isNewBlock || isNewSuperBlock && nextChallenge;
+ return isNewBlockRequired ? nextChallenge.block : null;
+ });
+ const onBlockSelect = actions::ofType(types.fetchNewBlock.start)
+ .map(({ payload }) => payload);
+ return Observable.merge(onAppMount, onNewChallenge, onBlockSelect)
+ .filter(block => {
+ const fullBlocks = fullBlocksSelector(getState());
+ return block && !fullBlocks.includes(block);
+ })
+ .flatMapLatest(blockName => {
+ const lang = langSelector(getState());
const options = {
params: { lang, blockName },
service: 'challenge'
};
return services.readService$(options)
.retry(3)
- .map(fetchChallengesCompleted)
- .startWith({ type: types.fetchChallenges.start })
+ .map(newBlockData => {
+ const { dashedName } = paramsSelector(getState());
+ const { entities: { challenge } } = newBlockData;
+ const currentChallengeInNewBlock = _.pickBy(
+ challenge,
+ newChallenge => newChallenge.dashedName === dashedName
+ );
+ return fetchNewBlockComplete({
+ ...newBlockData,
+ meta: {
+ challenge: currentChallengeInNewBlock
+ }
+ });
+ })
.catch(createErrorObservable);
- })
- .filter(Boolean);
-}
+ });
+ }
-function fetchChallengesForNextBlockEpic(action$, { getState }) {
- return action$::ofType(challenge.checkForNextBlock)
- .map(() => {
- const {
- nextChallenge,
- isNewBlock,
- isNewSuperBlock
- } = nextChallengeSelector(getState());
- const isNewBlockRequired = (
- (isNewBlock || isNewSuperBlock) &&
- nextChallenge &&
- !nextChallenge.description
- );
- return isNewBlockRequired ?
- fetchNewBlock(nextChallenge.block) :
- null;
- })
- .filter(Boolean);
-}
export default combineEpics(
fetchChallengeEpic,
- fetchChallengesForBlockEpic,
- fetchChallengesForNextBlockEpic
+ fetchChallengesForBlockEpic
);
diff --git a/common/app/redux/index.js b/common/app/redux/index.js
index d878ffc01f..4b49bd6b5c 100644
--- a/common/app/redux/index.js
+++ b/common/app/redux/index.js
@@ -15,14 +15,18 @@ import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
import fetchChallengesEpic from './fetch-challenges-epic.js';
import nightModeEpic from './night-mode-epic.js';
-import { createFilesMetaCreator } from '../files';
-import { updateThemeMetacreator, entitiesSelector } from '../entities';
+import {
+ updateThemeMetacreator,
+ entitiesSelector,
+ fullBlocksSelector
+} from '../entities';
import { utils } from '../Flash/redux';
import { paramsSelector } from '../Router/redux';
import { types as challenges } from '../routes/Challenges/redux';
import { types as map } from '../Map/redux';
import {
- challengeToFiles,
+ createCurrentChallengeMeta,
+ challengeToFilesMetaCreator,
getFirstChallengeOfNextBlock,
getFirstChallengeOfNextSuperBlock,
getNextChallenge
@@ -113,7 +117,7 @@ export const fetchChallengeCompleted = createAction(
null,
meta => ({
...meta,
- ...flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
+ ...challengeToFilesMetaCreator(meta.challenge)
})
);
export const fetchChallenges = createAction('' + types.fetchChallenges);
@@ -124,7 +128,8 @@ export const fetchChallengesCompleted = createAction(
export const fetchNewBlock = createAction(types.fetchNewBlock.start);
export const fetchNewBlockComplete = createAction(
types.fetchNewBlock.complete,
- ({ entities }) => entities
+ ({ entities }) => ({ entities }),
+ ({ meta: { challenge } }) => ({ ...createCurrentChallengeMeta(challenge) })
);
export const updateChallenges = createAction(types.updateChallenges);
@@ -239,6 +244,12 @@ export const challengeSelector = state => {
return challengeMap[challengeName] || {};
};
+export const isCurrentBlockCompleteSelector = state => {
+ const { block } = paramsSelector(state);
+ const fullBlocks = fullBlocksSelector(state);
+ return fullBlocks.includes(block);
+};
+
export const previousSolutionSelector = state => {
const { id } = challengeSelector(state);
const { challengeMap = {} } = userSelector(state);
diff --git a/common/app/routes/Challenges/Challenge-Description.jsx b/common/app/routes/Challenges/Challenge-Description.jsx
index 0aedaa13d3..f05bb4dc5f 100644
--- a/common/app/routes/Challenges/Challenge-Description.jsx
+++ b/common/app/routes/Challenges/Challenge-Description.jsx
@@ -1,21 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
import { Col, Row } from 'react-bootstrap';
import ns from './ns.json';
+import { isCurrentBlockCompleteSelector } from '../../redux';
+import { SkeletonSprite } from '../../helperComponents';
+
+const mapStateToProps = createSelector(
+ isCurrentBlockCompleteSelector,
+ blockComplete => ({
+ showLoading: !blockComplete
+ })
+);
const propTypes = {
- children: PropTypes.array
+ children: PropTypes.array,
+ showLoading: PropTypes.bool
};
-export default function ChallengeDescription({ children }) {
+function ChallengeDescription({ children, showLoading }) {
return (
- { children }
+ {
+ showLoading ?
+ children
+ .map((_, i) => (
+
+
+
+ )) :
+ children
+ }
);
@@ -23,3 +47,5 @@ export default function ChallengeDescription({ children }) {
ChallengeDescription.displayName = 'ChallengeDescription';
ChallengeDescription.propTypes = propTypes;
+
+export default connect(mapStateToProps)(ChallengeDescription);
diff --git a/common/app/routes/Challenges/Challenge-Title.jsx b/common/app/routes/Challenges/Challenge-Title.jsx
index a2c98f81d0..e45937f8cc 100644
--- a/common/app/routes/Challenges/Challenge-Title.jsx
+++ b/common/app/routes/Challenges/Challenge-Title.jsx
@@ -1,15 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
import ns from './ns.json';
+import { isCurrentBlockCompleteSelector } from '../../redux';
+import { SkeletonSprite } from '../../helperComponents';
+const mapStateToProps = createSelector(
+ isCurrentBlockCompleteSelector,
+ blockComplete => ({
+ showLoading: !blockComplete
+ })
+);
const propTypes = {
children: PropTypes.string,
- isCompleted: PropTypes.bool
+ isCompleted: PropTypes.bool,
+ showLoading: PropTypes.bool
};
-export default function ChallengeTitle({ children, isCompleted }) {
+function ChallengeTitle({ children, isCompleted, showLoading }) {
let icon = null;
+ if (showLoading) {
+ return (
+
+
+
+
+ );
+ }
if (isCompleted) {
icon = (
({
- challenge,
- showLoading: !fullBlocks.includes(block)
- })
-);
-
-const mapDispatchToProps = { challengeUpdated };
const propTypes = {
challenge: PropTypes.object,
- challengeUpdated: PropTypes.func.isRequired,
children: PropTypes.node,
showLoading: PropTypes.bool
};
-class ChildContainer extends PureComponent {
- componentDidUpdate(prevProps) {
- const { challenge = {}, challengeUpdated } = this.props;
- if (prevProps.showLoading && !this.props.showLoading) {
- challengeUpdated(challenge);
- }
- }
- render() {
- const { children, showLoading, ...props } = this.props;
- return (
-
- {
- showLoading ? : null
- }
- { children }
-
-
- );
- }
+function ChildContainer(props) {
+ const { children, ...restProps } = props;
+ return (
+
+ { children }
+
+
+ );
}
ChildContainer.propTypes = propTypes;
-export default connect(mapStateToProps, mapDispatchToProps)(ChildContainer);
+export default ChildContainer;
diff --git a/common/app/routes/Challenges/Code-Mirror-Skeleton.jsx b/common/app/routes/Challenges/Code-Mirror-Skeleton.jsx
index 02567457ca..a2d51ac7de 100644
--- a/common/app/routes/Challenges/Code-Mirror-Skeleton.jsx
+++ b/common/app/routes/Challenges/Code-Mirror-Skeleton.jsx
@@ -1,8 +1,6 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import { Grid, Col, Row } from 'react-bootstrap';
-
-import ns from './ns.json';
+import { SkeletonSprite } from '../../helperComponents';
const propTypes = {
content: PropTypes.string
@@ -10,19 +8,6 @@ const propTypes = {
export default class CodeMirrorSkeleton extends PureComponent {
- renderLine(line, i) {
- return (
-
- );
- }
render() {
const {
@@ -39,18 +24,12 @@ export default class CodeMirrorSkeleton extends PureComponent {
className='CodeMirror-sizer'
style={
{
- minHeight: (editorLines.length * 18) + 'px',
+ height: (editorLines.length * 18) + 'px',
overflow: 'hidden'
}
}
>
-
-
-
- { editorLines.map(this.renderLine) }
-
-
-
+
diff --git a/common/app/routes/Challenges/challenges.less b/common/app/routes/Challenges/challenges.less
index c57d2ab4eb..2a70198e69 100644
--- a/common/app/routes/Challenges/challenges.less
+++ b/common/app/routes/Challenges/challenges.less
@@ -142,48 +142,6 @@
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;
diff --git a/common/app/routes/Challenges/redux/index.js b/common/app/routes/Challenges/redux/index.js
index b7c3d14135..bfbdb0d87a 100644
--- a/common/app/routes/Challenges/redux/index.js
+++ b/common/app/routes/Challenges/redux/index.js
@@ -28,7 +28,8 @@ import {
submitTypes,
viewTypes,
getFileKey,
- challengeToFiles
+
+ challengeToFilesMetaCreator
} from '../utils';
import {
types as app,
@@ -36,14 +37,11 @@ import {
} from '../../../redux';
import { html } from '../../../utils/challengeTypes.js';
import blockNameify from '../../../utils/blockNameify.js';
-import { updateFileMetaCreator, createFilesMetaCreator } from '../../../files';
+import { updateFileMetaCreator } from '../../../files';
// this is not great but is ok until we move to a different form type
export projectNormalizer from '../views/project/redux';
-const challengeToFilesMetaCreator =
- _.flow(challengeToFiles, createFilesMetaCreator);
-
export const epics = [
modalEpic,
challengeEpic,
diff --git a/common/app/routes/Challenges/utils/index.js b/common/app/routes/Challenges/utils/index.js
index 516fb47701..488f4e7303 100644
--- a/common/app/routes/Challenges/utils/index.js
+++ b/common/app/routes/Challenges/utils/index.js
@@ -3,6 +3,7 @@ import _ from 'lodash';
import * as challengeTypes from '../../../utils/challengeTypes.js';
import { createPoly, updateFileFromSpec } from '../../../../utils/polyvinyl.js';
import { decodeScriptTags } from '../../../../utils/encode-decode.js';
+import { createFilesMetaCreator } from '../../../files';
// turn challengeType to file ext
const pathsMap = {
@@ -113,6 +114,17 @@ export function challengeToFiles(challenge, files) {
};
}
+export const challengeToFilesMetaCreator =
+_.flow(challengeToFiles, createFilesMetaCreator);
+
+// ({ dashedName: { Challenge } }) => ({ meta: Files }) || {}
+export function createCurrentChallengeMeta(challenge) {
+ if (_.isEmpty(challenge)) {
+ return {};
+ }
+ return challengeToFilesMetaCreator(_.values(challenge)[0]);
+}
+
export function createTests({ tests = [] }) {
return tests
.map(test => {