fix(combineEpics): Compine new block epics in to one
This commit is contained in:
@ -191,11 +191,11 @@ export default composeReducers(
|
|||||||
() => ({
|
() => ({
|
||||||
[
|
[
|
||||||
combineActions(
|
combineActions(
|
||||||
app.fetchChallenges.complete,
|
app.fetchNewBlock.complete,
|
||||||
map.fetchMapUi.complete
|
map.fetchMapUi.complete
|
||||||
)
|
)
|
||||||
]: (state, { payload: { entities } }) => merge({}, state, entities),
|
]: (state, { payload: { entities } }) => merge({}, state, entities),
|
||||||
[app.fetchChallenges.complete]:
|
[app.fetchNewBlock.complete]:
|
||||||
(state, { payload: { entities: { block }}}) => ({
|
(state, { payload: { entities: { block }}}) => ({
|
||||||
...state,
|
...state,
|
||||||
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
|
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import styles from './overlayLoader-styles';
|
|
||||||
|
|
||||||
function LoaderCircle({ delay, origin }, i) {
|
|
||||||
return (
|
|
||||||
<circle
|
|
||||||
className='innerCircle'
|
|
||||||
cx='50%'
|
|
||||||
cy='50%'
|
|
||||||
fill='transparent'
|
|
||||||
key={ i }
|
|
||||||
r='5%'
|
|
||||||
stroke='#006400'
|
|
||||||
strokeWidth='3'
|
|
||||||
style={{ animationDelay: '' + delay, transformOrigin: '' + origin }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className='svg-container'>
|
|
||||||
<style dangerouslySetInnerHTML={{ __html: styles }} />
|
|
||||||
<svg className='svg' height='100%' width='100%'>
|
|
||||||
{
|
|
||||||
animationProps.map(LoaderCircle)
|
|
||||||
}
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayLoader.displayName = 'OverlayLoader';
|
|
||||||
|
|
||||||
export default OverlayLoader;
|
|
26
common/app/helperComponents/SkeletonSprite.jsx
Normal file
26
common/app/helperComponents/SkeletonSprite.jsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styles from './skeletonStyles';
|
||||||
|
|
||||||
|
function SkeletonSprite() {
|
||||||
|
return (
|
||||||
|
<div className='sprite-container'>
|
||||||
|
<style dangerouslySetInnerHTML={ { __html: styles } } />
|
||||||
|
<svg className='sprite-svg'>
|
||||||
|
<rect
|
||||||
|
className='sprite'
|
||||||
|
fill='#ccc'
|
||||||
|
height='100%'
|
||||||
|
stroke='#ccc'
|
||||||
|
width='2px'
|
||||||
|
x='0'
|
||||||
|
y='0'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkeletonSprite.displayName = 'SkeletonSprite';
|
||||||
|
|
||||||
|
export default SkeletonSprite;
|
@ -1,5 +1,5 @@
|
|||||||
|
export { default as ButtonSpacer } from './ButtonSpacer.jsx';
|
||||||
export { default as FullWidthRow } from './FullWidthRow.jsx';
|
export { default as FullWidthRow } from './FullWidthRow.jsx';
|
||||||
export { default as Loader } from './Loader.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 Spacer } from './Spacer.jsx';
|
||||||
export { default as ButtonSpacer } from './ButtonSpacer.jsx';
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
60
common/app/helperComponents/skeletonStyles.js
Normal file
60
common/app/helperComponents/skeletonStyles.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
`;
|
@ -1,5 +1,6 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import { combineEpics, ofType } from 'redux-epic';
|
import { combineEpics, ofType } from 'redux-epic';
|
||||||
|
import _ from 'lodash';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -9,8 +10,7 @@ import {
|
|||||||
delayedRedirect,
|
delayedRedirect,
|
||||||
|
|
||||||
fetchChallengeCompleted,
|
fetchChallengeCompleted,
|
||||||
fetchChallengesCompleted,
|
fetchNewBlockComplete,
|
||||||
fetchNewBlock,
|
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
nextChallengeSelector
|
nextChallengeSelector
|
||||||
} from './';
|
} from './';
|
||||||
@ -21,7 +21,7 @@ import {
|
|||||||
|
|
||||||
import { shapeChallenges } from './utils';
|
import { shapeChallenges } from './utils';
|
||||||
import { types as challenge } from '../routes/Challenges/redux';
|
import { types as challenge } from '../routes/Challenges/redux';
|
||||||
import { langSelector } from '../Router/redux';
|
import { langSelector, paramsSelector } from '../Router/redux';
|
||||||
|
|
||||||
const isDev = debug.enabled('fcc:*');
|
const isDev = debug.enabled('fcc:*');
|
||||||
|
|
||||||
@ -60,61 +60,59 @@ export function fetchChallengesForBlockEpic(
|
|||||||
{ getState },
|
{ getState },
|
||||||
{ services }
|
{ services }
|
||||||
) {
|
) {
|
||||||
return actions::ofType(
|
const onAppMount = actions::ofType(types.appMounted)
|
||||||
types.appMounted,
|
.map(() => {
|
||||||
types.updateChallenges,
|
const {
|
||||||
types.fetchNewBlock.start
|
block = 'basic-html-and-html5'
|
||||||
)
|
} = challengeSelector(getState());
|
||||||
.flatMapLatest(({ type, payload }) => {
|
return block;
|
||||||
const fetchAnotherBlock = type === types.fetchNewBlock.start;
|
});
|
||||||
const state = getState();
|
const onNewChallenge = actions::ofType(challenge.moveToNextChallenge)
|
||||||
let {
|
.map(() => {
|
||||||
block: blockName = 'basic-html-and-html5'
|
const {
|
||||||
} = challengeSelector(state);
|
isNewBlock,
|
||||||
const lang = langSelector(state);
|
isNewSuperBlock,
|
||||||
if (fetchAnotherBlock) {
|
nextChallenge
|
||||||
const fullBlocks = fullBlocksSelector(state);
|
} = nextChallengeSelector(getState());
|
||||||
if (fullBlocks.includes(payload)) {
|
const isNewBlockRequired = isNewBlock || isNewSuperBlock && nextChallenge;
|
||||||
return Observable.of(null);
|
return isNewBlockRequired ? nextChallenge.block : null;
|
||||||
}
|
});
|
||||||
blockName = payload;
|
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 = {
|
const options = {
|
||||||
params: { lang, blockName },
|
params: { lang, blockName },
|
||||||
service: 'challenge'
|
service: 'challenge'
|
||||||
};
|
};
|
||||||
return services.readService$(options)
|
return services.readService$(options)
|
||||||
.retry(3)
|
.retry(3)
|
||||||
.map(fetchChallengesCompleted)
|
.map(newBlockData => {
|
||||||
.startWith({ type: types.fetchChallenges.start })
|
const { dashedName } = paramsSelector(getState());
|
||||||
|
const { entities: { challenge } } = newBlockData;
|
||||||
|
const currentChallengeInNewBlock = _.pickBy(
|
||||||
|
challenge,
|
||||||
|
newChallenge => newChallenge.dashedName === dashedName
|
||||||
|
);
|
||||||
|
return fetchNewBlockComplete({
|
||||||
|
...newBlockData,
|
||||||
|
meta: {
|
||||||
|
challenge: currentChallengeInNewBlock
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
.catch(createErrorObservable);
|
.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(
|
export default combineEpics(
|
||||||
fetchChallengeEpic,
|
fetchChallengeEpic,
|
||||||
fetchChallengesForBlockEpic,
|
fetchChallengesForBlockEpic
|
||||||
fetchChallengesForNextBlockEpic
|
|
||||||
);
|
);
|
||||||
|
@ -15,14 +15,18 @@ import updateMyCurrentChallengeEpic from './update-my-challenge-epic.js';
|
|||||||
import fetchChallengesEpic from './fetch-challenges-epic.js';
|
import fetchChallengesEpic from './fetch-challenges-epic.js';
|
||||||
import nightModeEpic from './night-mode-epic.js';
|
import nightModeEpic from './night-mode-epic.js';
|
||||||
|
|
||||||
import { createFilesMetaCreator } from '../files';
|
import {
|
||||||
import { updateThemeMetacreator, entitiesSelector } from '../entities';
|
updateThemeMetacreator,
|
||||||
|
entitiesSelector,
|
||||||
|
fullBlocksSelector
|
||||||
|
} from '../entities';
|
||||||
import { utils } from '../Flash/redux';
|
import { utils } from '../Flash/redux';
|
||||||
import { paramsSelector } from '../Router/redux';
|
import { paramsSelector } from '../Router/redux';
|
||||||
import { types as challenges } from '../routes/Challenges/redux';
|
import { types as challenges } from '../routes/Challenges/redux';
|
||||||
import { types as map } from '../Map/redux';
|
import { types as map } from '../Map/redux';
|
||||||
import {
|
import {
|
||||||
challengeToFiles,
|
createCurrentChallengeMeta,
|
||||||
|
challengeToFilesMetaCreator,
|
||||||
getFirstChallengeOfNextBlock,
|
getFirstChallengeOfNextBlock,
|
||||||
getFirstChallengeOfNextSuperBlock,
|
getFirstChallengeOfNextSuperBlock,
|
||||||
getNextChallenge
|
getNextChallenge
|
||||||
@ -113,7 +117,7 @@ export const fetchChallengeCompleted = createAction(
|
|||||||
null,
|
null,
|
||||||
meta => ({
|
meta => ({
|
||||||
...meta,
|
...meta,
|
||||||
...flow(challengeToFiles, createFilesMetaCreator)(meta.challenge)
|
...challengeToFilesMetaCreator(meta.challenge)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
export const fetchChallenges = createAction('' + types.fetchChallenges);
|
export const fetchChallenges = createAction('' + types.fetchChallenges);
|
||||||
@ -124,7 +128,8 @@ export const fetchChallengesCompleted = createAction(
|
|||||||
export const fetchNewBlock = createAction(types.fetchNewBlock.start);
|
export const fetchNewBlock = createAction(types.fetchNewBlock.start);
|
||||||
export const fetchNewBlockComplete = createAction(
|
export const fetchNewBlockComplete = createAction(
|
||||||
types.fetchNewBlock.complete,
|
types.fetchNewBlock.complete,
|
||||||
({ entities }) => entities
|
({ entities }) => ({ entities }),
|
||||||
|
({ meta: { challenge } }) => ({ ...createCurrentChallengeMeta(challenge) })
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateChallenges = createAction(types.updateChallenges);
|
export const updateChallenges = createAction(types.updateChallenges);
|
||||||
@ -239,6 +244,12 @@ export const challengeSelector = state => {
|
|||||||
return challengeMap[challengeName] || {};
|
return challengeMap[challengeName] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isCurrentBlockCompleteSelector = state => {
|
||||||
|
const { block } = paramsSelector(state);
|
||||||
|
const fullBlocks = fullBlocksSelector(state);
|
||||||
|
return fullBlocks.includes(block);
|
||||||
|
};
|
||||||
|
|
||||||
export const previousSolutionSelector = state => {
|
export const previousSolutionSelector = state => {
|
||||||
const { id } = challengeSelector(state);
|
const { id } = challengeSelector(state);
|
||||||
const { challengeMap = {} } = userSelector(state);
|
const { challengeMap = {} } = userSelector(state);
|
||||||
|
@ -1,21 +1,45 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
import { Col, Row } from 'react-bootstrap';
|
import { Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
import ns from './ns.json';
|
import ns from './ns.json';
|
||||||
|
import { isCurrentBlockCompleteSelector } from '../../redux';
|
||||||
|
import { SkeletonSprite } from '../../helperComponents';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
isCurrentBlockCompleteSelector,
|
||||||
|
blockComplete => ({
|
||||||
|
showLoading: !blockComplete
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.array
|
children: PropTypes.array,
|
||||||
|
showLoading: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ChallengeDescription({ children }) {
|
function ChallengeDescription({ children, showLoading }) {
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
className={ `${ns}-instructions` }
|
className={ `${ns}-instructions` }
|
||||||
xs={ 12 }
|
xs={ 12 }
|
||||||
>
|
>
|
||||||
{ children }
|
{
|
||||||
|
showLoading ?
|
||||||
|
children
|
||||||
|
.map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={ '' + i + 'description' }
|
||||||
|
style={{ height: '36px', margin: '9px 0px' }}
|
||||||
|
>
|
||||||
|
<SkeletonSprite />
|
||||||
|
</div>
|
||||||
|
)) :
|
||||||
|
children
|
||||||
|
}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
@ -23,3 +47,5 @@ export default function ChallengeDescription({ children }) {
|
|||||||
|
|
||||||
ChallengeDescription.displayName = 'ChallengeDescription';
|
ChallengeDescription.displayName = 'ChallengeDescription';
|
||||||
ChallengeDescription.propTypes = propTypes;
|
ChallengeDescription.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ChallengeDescription);
|
||||||
|
@ -1,15 +1,34 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import ns from './ns.json';
|
import ns from './ns.json';
|
||||||
|
import { isCurrentBlockCompleteSelector } from '../../redux';
|
||||||
|
import { SkeletonSprite } from '../../helperComponents';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
isCurrentBlockCompleteSelector,
|
||||||
|
blockComplete => ({
|
||||||
|
showLoading: !blockComplete
|
||||||
|
})
|
||||||
|
);
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.string,
|
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;
|
let icon = null;
|
||||||
|
if (showLoading) {
|
||||||
|
return (
|
||||||
|
<h4 style={{ height: '35px', marginBottom: '9px' }}>
|
||||||
|
<SkeletonSprite />
|
||||||
|
<hr />
|
||||||
|
</h4>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
icon = (
|
icon = (
|
||||||
<i
|
<i
|
||||||
@ -29,3 +48,5 @@ export default function ChallengeTitle({ children, isCompleted }) {
|
|||||||
|
|
||||||
ChallengeTitle.displayName = 'ChallengeTitle';
|
ChallengeTitle.displayName = 'ChallengeTitle';
|
||||||
ChallengeTitle.propTypes = propTypes;
|
ChallengeTitle.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ChallengeTitle);
|
||||||
|
@ -1,57 +1,25 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import { challengeSelector } from '../../redux';
|
|
||||||
import { challengeUpdated } from './redux';
|
|
||||||
|
|
||||||
import CompletionModal from './Completion-Modal.jsx';
|
import CompletionModal from './Completion-Modal.jsx';
|
||||||
import AppChildContainer from '../../Child-Container.jsx';
|
import AppChildContainer from '../../Child-Container.jsx';
|
||||||
import { OverlayLoader } from '../../helperComponents';
|
|
||||||
import { fullBlocksSelector } from '../../entities';
|
|
||||||
import { paramsSelector } from '../../Router/redux';
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
challengeSelector,
|
|
||||||
fullBlocksSelector,
|
|
||||||
paramsSelector,
|
|
||||||
(challenge, fullBlocks, { block }) => ({
|
|
||||||
challenge,
|
|
||||||
showLoading: !fullBlocks.includes(block)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const mapDispatchToProps = { challengeUpdated };
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
challenge: PropTypes.object,
|
challenge: PropTypes.object,
|
||||||
challengeUpdated: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
showLoading: PropTypes.bool
|
showLoading: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
class ChildContainer extends PureComponent {
|
function ChildContainer(props) {
|
||||||
componentDidUpdate(prevProps) {
|
const { children, ...restProps } = props;
|
||||||
const { challenge = {}, challengeUpdated } = this.props;
|
return (
|
||||||
if (prevProps.showLoading && !this.props.showLoading) {
|
<AppChildContainer { ...restProps }>
|
||||||
challengeUpdated(challenge);
|
{ children }
|
||||||
}
|
<CompletionModal />
|
||||||
}
|
</AppChildContainer>
|
||||||
render() {
|
);
|
||||||
const { children, showLoading, ...props } = this.props;
|
|
||||||
return (
|
|
||||||
<AppChildContainer { ...props }>
|
|
||||||
{
|
|
||||||
showLoading ? <OverlayLoader /> : null
|
|
||||||
}
|
|
||||||
{ children }
|
|
||||||
<CompletionModal />
|
|
||||||
</AppChildContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChildContainer.propTypes = propTypes;
|
ChildContainer.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChildContainer);
|
export default ChildContainer;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Grid, Col, Row } from 'react-bootstrap';
|
import { SkeletonSprite } from '../../helperComponents';
|
||||||
|
|
||||||
import ns from './ns.json';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
content: PropTypes.string
|
content: PropTypes.string
|
||||||
@ -10,19 +8,6 @@ const propTypes = {
|
|||||||
|
|
||||||
export default class CodeMirrorSkeleton extends PureComponent {
|
export default class CodeMirrorSkeleton extends PureComponent {
|
||||||
|
|
||||||
renderLine(line, i) {
|
|
||||||
return (
|
|
||||||
<div className={ `${ns}-shimmer` } key={ i }>
|
|
||||||
<Row>
|
|
||||||
<Col xs={ 12 }>
|
|
||||||
<div className='sprite-wrapper'>
|
|
||||||
<div className='sprite' />
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@ -39,18 +24,12 @@ export default class CodeMirrorSkeleton extends PureComponent {
|
|||||||
className='CodeMirror-sizer'
|
className='CodeMirror-sizer'
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
minHeight: (editorLines.length * 18) + 'px',
|
height: (editorLines.length * 18) + 'px',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='CodeMirror-lines'>
|
<SkeletonSprite />
|
||||||
<div className='CodeMirror-code'>
|
|
||||||
<Grid>
|
|
||||||
{ editorLines.map(this.renderLine) }
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,48 +142,6 @@
|
|||||||
word-wrap: break-word;
|
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 {
|
.@{ns}-success-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -28,7 +28,8 @@ import {
|
|||||||
submitTypes,
|
submitTypes,
|
||||||
viewTypes,
|
viewTypes,
|
||||||
getFileKey,
|
getFileKey,
|
||||||
challengeToFiles
|
|
||||||
|
challengeToFilesMetaCreator
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {
|
import {
|
||||||
types as app,
|
types as app,
|
||||||
@ -36,14 +37,11 @@ import {
|
|||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
import { html } from '../../../utils/challengeTypes.js';
|
import { html } from '../../../utils/challengeTypes.js';
|
||||||
import blockNameify from '../../../utils/blockNameify.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
|
// this is not great but is ok until we move to a different form type
|
||||||
export projectNormalizer from '../views/project/redux';
|
export projectNormalizer from '../views/project/redux';
|
||||||
|
|
||||||
const challengeToFilesMetaCreator =
|
|
||||||
_.flow(challengeToFiles, createFilesMetaCreator);
|
|
||||||
|
|
||||||
export const epics = [
|
export const epics = [
|
||||||
modalEpic,
|
modalEpic,
|
||||||
challengeEpic,
|
challengeEpic,
|
||||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||||||
import * as challengeTypes from '../../../utils/challengeTypes.js';
|
import * as challengeTypes from '../../../utils/challengeTypes.js';
|
||||||
import { createPoly, updateFileFromSpec } from '../../../../utils/polyvinyl.js';
|
import { createPoly, updateFileFromSpec } from '../../../../utils/polyvinyl.js';
|
||||||
import { decodeScriptTags } from '../../../../utils/encode-decode.js';
|
import { decodeScriptTags } from '../../../../utils/encode-decode.js';
|
||||||
|
import { createFilesMetaCreator } from '../../../files';
|
||||||
|
|
||||||
// turn challengeType to file ext
|
// turn challengeType to file ext
|
||||||
const pathsMap = {
|
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 = [] }) {
|
export function createTests({ tests = [] }) {
|
||||||
return tests
|
return tests
|
||||||
.map(test => {
|
.map(test => {
|
||||||
|
Reference in New Issue
Block a user