fix(combineEpics): Compine new block epics in to one
This commit is contained in:
@ -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] ])
|
||||
|
@ -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 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';
|
||||
|
@ -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 { 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 })
|
||||
.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
|
||||
.map(newBlockData => {
|
||||
const { dashedName } = paramsSelector(getState());
|
||||
const { entities: { challenge } } = newBlockData;
|
||||
const currentChallengeInNewBlock = _.pickBy(
|
||||
challenge,
|
||||
newChallenge => newChallenge.dashedName === dashedName
|
||||
);
|
||||
return isNewBlockRequired ?
|
||||
fetchNewBlock(nextChallenge.block) :
|
||||
null;
|
||||
return fetchNewBlockComplete({
|
||||
...newBlockData,
|
||||
meta: {
|
||||
challenge: currentChallengeInNewBlock
|
||||
}
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
.catch(createErrorObservable);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default combineEpics(
|
||||
fetchChallengeEpic,
|
||||
fetchChallengesForBlockEpic,
|
||||
fetchChallengesForNextBlockEpic
|
||||
fetchChallengesForBlockEpic
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<Row>
|
||||
<Col
|
||||
className={ `${ns}-instructions` }
|
||||
xs={ 12 }
|
||||
>
|
||||
{ children }
|
||||
{
|
||||
showLoading ?
|
||||
children
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={ '' + i + 'description' }
|
||||
style={{ height: '36px', margin: '9px 0px' }}
|
||||
>
|
||||
<SkeletonSprite />
|
||||
</div>
|
||||
)) :
|
||||
children
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
@ -23,3 +47,5 @@ export default function ChallengeDescription({ children }) {
|
||||
|
||||
ChallengeDescription.displayName = 'ChallengeDescription';
|
||||
ChallengeDescription.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps)(ChallengeDescription);
|
||||
|
@ -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 (
|
||||
<h4 style={{ height: '35px', marginBottom: '9px' }}>
|
||||
<SkeletonSprite />
|
||||
<hr />
|
||||
</h4>
|
||||
);
|
||||
}
|
||||
if (isCompleted) {
|
||||
icon = (
|
||||
<i
|
||||
@ -29,3 +48,5 @@ export default function ChallengeTitle({ children, isCompleted }) {
|
||||
|
||||
ChallengeTitle.displayName = 'ChallengeTitle';
|
||||
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 { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { challengeSelector } from '../../redux';
|
||||
import { challengeUpdated } from './redux';
|
||||
|
||||
import CompletionModal from './Completion-Modal.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 = {
|
||||
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;
|
||||
function ChildContainer(props) {
|
||||
const { children, ...restProps } = props;
|
||||
return (
|
||||
<AppChildContainer { ...props }>
|
||||
{
|
||||
showLoading ? <OverlayLoader /> : null
|
||||
}
|
||||
<AppChildContainer { ...restProps }>
|
||||
{ children }
|
||||
<CompletionModal />
|
||||
</AppChildContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChildContainer.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChildContainer);
|
||||
export default ChildContainer;
|
||||
|
@ -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 (
|
||||
<div className={ `${ns}-shimmer` } key={ i }>
|
||||
<Row>
|
||||
<Col xs={ 12 }>
|
||||
<div className='sprite-wrapper'>
|
||||
<div className='sprite' />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className='CodeMirror-lines'>
|
||||
<div className='CodeMirror-code'>
|
||||
<Grid>
|
||||
{ editorLines.map(this.renderLine) }
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonSprite />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 => {
|
||||
|
Reference in New Issue
Block a user