mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-05 00:00:18 -04:00
Merge pull request #16819 from Bouncey/fix/slowNetwork
fix(challenge): Handle slow network connections gracefully
This commit is contained in:
@@ -49,7 +49,11 @@ const primaryLang = getLangFromPath(location.pathname);
|
||||
|
||||
defaultState.app.csrfToken = csrfToken;
|
||||
|
||||
const serviceOptions = { xhrPath: '/services', context: { _csrf: csrfToken } };
|
||||
const serviceOptions = {
|
||||
context: { _csrf: csrfToken },
|
||||
xhrPath: '/services',
|
||||
xhrTimeout: 15000
|
||||
};
|
||||
|
||||
const history = useLangRoutes(createHistory, primaryLang)();
|
||||
sendPageAnalytics(history, ga);
|
||||
|
||||
@@ -337,6 +337,10 @@ p {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-big.btn-primary[disabled] {
|
||||
color: rgb(141, 139, 132)
|
||||
}
|
||||
|
||||
.btn-bigger {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -179,9 +179,7 @@ export default composeReducers(
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...merge(state, action.meta.entities)
|
||||
};
|
||||
return merge({}, state, action.meta.entities);
|
||||
}
|
||||
return state;
|
||||
},
|
||||
@@ -205,16 +203,15 @@ export default composeReducers(
|
||||
() => ({
|
||||
[
|
||||
combineActions(
|
||||
app.fetchChallenges.complete,
|
||||
app.fetchNewBlock.complete,
|
||||
map.fetchMapUi.complete
|
||||
)
|
||||
]: (state, { payload }) => {
|
||||
const {entities: { block } } = payload;
|
||||
return {
|
||||
...merge(state, payload.entities),
|
||||
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
|
||||
};
|
||||
},
|
||||
]: (state, { payload: { entities } }) => merge({}, state, entities),
|
||||
[app.fetchNewBlock.complete]:
|
||||
(state, { payload: { entities: { block }}}) => ({
|
||||
...state,
|
||||
fullBlocks: union(state.fullBlocks, [ Object.keys(block)[0] ])
|
||||
}),
|
||||
[
|
||||
challenges.submitChallenge.complete
|
||||
]: (state, { payload: { username, points, challengeInfo } }) => ({
|
||||
|
||||
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,4 +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 SkeletonSprite } from './SkeletonSprite.jsx';
|
||||
export { default as Spacer } from './Spacer.jsx';
|
||||
export { default as ButtonSpacer } from './ButtonSpacer.jsx';
|
||||
|
||||
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,62 +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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,9 +8,10 @@ const propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default function ChildContainer({ children, ...props }) {
|
||||
function ChildContainer(props) {
|
||||
const { children, ...restProps } = props;
|
||||
return (
|
||||
<AppChildContainer { ...props }>
|
||||
<AppChildContainer { ...restProps }>
|
||||
{ children }
|
||||
<CompletionModal />
|
||||
</AppChildContainer>
|
||||
@@ -18,3 +19,5 @@ export default function ChildContainer({ children, ...props }) {
|
||||
}
|
||||
|
||||
ChildContainer.propTypes = propTypes;
|
||||
|
||||
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>
|
||||
|
||||
@@ -73,6 +73,7 @@ const propTypes = {
|
||||
dashedName: PropTypes.string,
|
||||
lang: PropTypes.string.isRequired
|
||||
}),
|
||||
showLoading: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
updateSuccessMessage: PropTypes.func.isRequired,
|
||||
updateTitle: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button, Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||
import { isCurrentBlockCompleteSelector } from '../../redux';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isCurrentBlockCompleteSelector,
|
||||
blockComplete => ({
|
||||
isDisabled: !blockComplete
|
||||
})
|
||||
);
|
||||
|
||||
const unlockWarning = (
|
||||
<Tooltip id='tooltip'>
|
||||
@@ -15,13 +25,14 @@ const propTypes = {
|
||||
guideUrl: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
isCodeLocked: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool,
|
||||
makeToast: PropTypes.func.isRequired,
|
||||
openHelpModal: PropTypes.func.isRequired,
|
||||
unlockUntrustedCode: PropTypes.func.isRequired,
|
||||
updateHint: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default class ToolPanel extends PureComponent {
|
||||
class ToolPanel extends PureComponent {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.makeHint = this.makeHint.bind(this);
|
||||
@@ -44,7 +55,7 @@ export default class ToolPanel extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
renderHint(hint, makeHint) {
|
||||
renderHint(hint, makeHint, isDisabled) {
|
||||
if (!hint) {
|
||||
return null;
|
||||
}
|
||||
@@ -53,6 +64,7 @@ export default class ToolPanel extends PureComponent {
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
disabled={ isDisabled }
|
||||
onClick={ makeHint }
|
||||
>
|
||||
Hint
|
||||
@@ -60,7 +72,12 @@ export default class ToolPanel extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderExecute(isCodeLocked, executeChallenge, unlockUntrustedCode) {
|
||||
renderExecute(
|
||||
isCodeLocked,
|
||||
executeChallenge,
|
||||
unlockUntrustedCode,
|
||||
isDisabled
|
||||
) {
|
||||
if (isCodeLocked) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
@@ -71,6 +88,7 @@ export default class ToolPanel extends PureComponent {
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
disabled={ isDisabled }
|
||||
onClick={ unlockUntrustedCode }
|
||||
>
|
||||
I trust this code. Unlock it.
|
||||
@@ -83,6 +101,7 @@ export default class ToolPanel extends PureComponent {
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
disabled={ isDisabled }
|
||||
onClick={ executeChallenge }
|
||||
>
|
||||
Run tests (ctrl + enter)
|
||||
@@ -96,18 +115,20 @@ export default class ToolPanel extends PureComponent {
|
||||
guideUrl,
|
||||
hint,
|
||||
isCodeLocked,
|
||||
isDisabled,
|
||||
openHelpModal,
|
||||
unlockUntrustedCode
|
||||
} = this.props;
|
||||
|
||||
console.log(isDisabled);
|
||||
return (
|
||||
<div>
|
||||
{ this.renderHint(hint, this.makeHint) }
|
||||
{ this.renderHint(hint, this.makeHintm, isDisabled) }
|
||||
{
|
||||
this.renderExecute(
|
||||
isCodeLocked,
|
||||
executeChallenge,
|
||||
unlockUntrustedCode
|
||||
unlockUntrustedCode,
|
||||
isDisabled
|
||||
)
|
||||
}
|
||||
<div className='button-spacer' />
|
||||
@@ -115,6 +136,7 @@ export default class ToolPanel extends PureComponent {
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
disabled={ isDisabled }
|
||||
onClick={ this.makeReset }
|
||||
>
|
||||
Reset your code
|
||||
@@ -127,6 +149,7 @@ export default class ToolPanel extends PureComponent {
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
disabled={ isDisabled }
|
||||
href={ guideUrl }
|
||||
target='_blank'
|
||||
>
|
||||
@@ -139,6 +162,7 @@ export default class ToolPanel extends PureComponent {
|
||||
block={ true }
|
||||
bsStyle='primary'
|
||||
className='btn-big'
|
||||
disabled={ isDisabled }
|
||||
onClick={ openHelpModal }
|
||||
>
|
||||
Ask for help on the forum
|
||||
@@ -151,3 +175,5 @@ export default class ToolPanel extends PureComponent {
|
||||
|
||||
ToolPanel.displayName = 'ToolPanel';
|
||||
ToolPanel.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps)(ToolPanel);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -31,7 +31,7 @@ const mapStateToProps = createSelector(
|
||||
actionCompletedSelector,
|
||||
lightBoxSelector,
|
||||
(
|
||||
{ description = [] },
|
||||
{ description = [['', '', 'Happy Coding!', '']] },
|
||||
currentIndex,
|
||||
previousIndex,
|
||||
isActionCompleted,
|
||||
|
||||
@@ -58,7 +58,8 @@ export default function mapUiService(app) {
|
||||
block,
|
||||
isLocked,
|
||||
isComingSoon,
|
||||
isBeta
|
||||
isBeta,
|
||||
challengeType
|
||||
} = challenge;
|
||||
map[dashedName] = {
|
||||
dashedName,
|
||||
@@ -68,7 +69,8 @@ export default function mapUiService(app) {
|
||||
block,
|
||||
isLocked,
|
||||
isComingSoon,
|
||||
isBeta
|
||||
isBeta,
|
||||
challengeType
|
||||
};
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
Reference in New Issue
Block a user