2018-12-11 23:05:15 +02:00
|
|
|
import React, { Component } from 'react';
|
2018-04-06 14:51:52 +01:00
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
import { bindActionCreators } from 'redux';
|
2018-12-11 23:05:15 +02:00
|
|
|
import { createStructuredSelector } from 'reselect';
|
2018-04-06 14:51:52 +01:00
|
|
|
import { connect } from 'react-redux';
|
|
|
|
import Helmet from 'react-helmet';
|
2018-09-11 16:10:21 +03:00
|
|
|
import { graphql } from 'gatsby';
|
2018-09-30 11:37:19 +01:00
|
|
|
import { first } from 'lodash';
|
2019-03-09 00:59:25 +03:00
|
|
|
import Media from 'react-responsive';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2018-09-30 11:37:19 +01:00
|
|
|
import LearnLayout from '../../../components/layouts/Learn';
|
2018-04-06 14:51:52 +01:00
|
|
|
import Editor from './Editor';
|
|
|
|
import Preview from '../components/Preview';
|
|
|
|
import SidePanel from '../components/Side-Panel';
|
2018-05-18 14:54:21 +01:00
|
|
|
import Output from '../components/Output';
|
2018-04-06 14:51:52 +01:00
|
|
|
import CompletionModal from '../components/CompletionModal';
|
2018-05-08 00:29:45 +01:00
|
|
|
import HelpModal from '../components/HelpModal';
|
2018-08-06 09:25:50 -04:00
|
|
|
import VideoModal from '../components/VideoModal';
|
2018-05-09 12:40:09 +01:00
|
|
|
import ResetModal from '../components/ResetModal';
|
2018-12-11 23:05:15 +02:00
|
|
|
import MobileLayout from './MobileLayout';
|
|
|
|
import DesktopLayout from './DesktopLayout';
|
2019-09-18 17:46:19 +02:00
|
|
|
import Hotkeys from '../components/Hotkeys';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2019-08-28 21:55:56 +05:30
|
|
|
import { getGuideUrl } from '../utils';
|
2018-04-08 23:19:50 +01:00
|
|
|
import { challengeTypes } from '../../../../utils/challengeTypes';
|
|
|
|
import { ChallengeNode } from '../../../redux/propTypes';
|
2019-09-25 20:16:08 +02:00
|
|
|
import { dasherize } from '../../../../../utils/slugs';
|
2018-04-06 14:51:52 +01:00
|
|
|
import {
|
|
|
|
createFiles,
|
|
|
|
challengeFilesSelector,
|
2018-05-18 14:54:21 +01:00
|
|
|
challengeTestsSelector,
|
2019-06-20 10:04:36 +02:00
|
|
|
initConsole,
|
2018-04-06 14:51:52 +01:00
|
|
|
initTests,
|
2018-05-09 12:40:09 +01:00
|
|
|
updateChallengeMeta,
|
2018-05-10 23:25:31 +01:00
|
|
|
challengeMounted,
|
2019-09-18 17:46:19 +02:00
|
|
|
consoleOutputSelector,
|
2020-02-03 15:22:49 +02:00
|
|
|
executeChallenge,
|
|
|
|
cancelTests
|
2018-04-08 23:19:50 +01:00
|
|
|
} from '../redux';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
|
|
|
import './classic.css';
|
2018-09-21 08:33:19 +03:00
|
|
|
import '../components/test-frame.css';
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2018-12-11 23:05:15 +02:00
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
|
|
files: challengeFilesSelector,
|
|
|
|
tests: challengeTestsSelector,
|
|
|
|
output: consoleOutputSelector
|
|
|
|
});
|
2018-04-06 14:51:52 +01:00
|
|
|
|
|
|
|
const mapDispatchToProps = dispatch =>
|
2018-05-09 12:40:09 +01:00
|
|
|
bindActionCreators(
|
2018-05-10 23:25:31 +01:00
|
|
|
{
|
|
|
|
createFiles,
|
2019-06-20 10:04:36 +02:00
|
|
|
initConsole,
|
2018-05-10 23:25:31 +01:00
|
|
|
initTests,
|
|
|
|
updateChallengeMeta,
|
2019-09-18 17:46:19 +02:00
|
|
|
challengeMounted,
|
2020-02-03 15:22:49 +02:00
|
|
|
executeChallenge,
|
|
|
|
cancelTests
|
2018-05-10 23:25:31 +01:00
|
|
|
},
|
2018-05-09 12:40:09 +01:00
|
|
|
dispatch
|
|
|
|
);
|
2018-04-06 14:51:52 +01:00
|
|
|
|
|
|
|
const propTypes = {
|
2020-02-03 15:22:49 +02:00
|
|
|
cancelTests: PropTypes.func.isRequired,
|
2018-05-09 12:40:09 +01:00
|
|
|
challengeMounted: PropTypes.func.isRequired,
|
2018-04-06 14:51:52 +01:00
|
|
|
createFiles: PropTypes.func.isRequired,
|
|
|
|
data: PropTypes.shape({
|
|
|
|
challengeNode: ChallengeNode
|
|
|
|
}),
|
2019-09-18 17:46:19 +02:00
|
|
|
executeChallenge: PropTypes.func.isRequired,
|
2018-04-06 14:51:52 +01:00
|
|
|
files: PropTypes.shape({
|
|
|
|
key: PropTypes.string
|
|
|
|
}),
|
2019-06-20 10:04:36 +02:00
|
|
|
initConsole: PropTypes.func.isRequired,
|
2018-04-06 14:51:52 +01:00
|
|
|
initTests: PropTypes.func.isRequired,
|
2020-07-17 21:03:23 +02:00
|
|
|
output: PropTypes.arrayOf(PropTypes.string),
|
2018-09-11 16:19:11 +03:00
|
|
|
pageContext: PropTypes.shape({
|
2019-09-17 15:32:23 +02:00
|
|
|
challengeMeta: PropTypes.shape({
|
|
|
|
id: PropTypes.string,
|
|
|
|
introPath: PropTypes.string,
|
|
|
|
nextChallengePath: PropTypes.string,
|
|
|
|
prevChallengePath: PropTypes.string
|
|
|
|
})
|
2018-04-06 14:51:52 +01:00
|
|
|
}),
|
2018-05-18 14:54:21 +01:00
|
|
|
tests: PropTypes.arrayOf(
|
|
|
|
PropTypes.shape({
|
|
|
|
text: PropTypes.string,
|
|
|
|
testString: PropTypes.string
|
|
|
|
})
|
|
|
|
),
|
2019-02-06 14:32:36 +03:00
|
|
|
updateChallengeMeta: PropTypes.func.isRequired
|
2018-04-06 14:51:52 +01:00
|
|
|
};
|
|
|
|
|
2018-12-07 14:08:32 +02:00
|
|
|
const MAX_MOBILE_WIDTH = 767;
|
|
|
|
|
2018-09-30 11:37:19 +01:00
|
|
|
class ShowClassic extends Component {
|
2018-05-27 09:37:46 +01:00
|
|
|
constructor() {
|
2018-05-29 15:34:50 +01:00
|
|
|
super();
|
2018-05-27 09:37:46 +01:00
|
|
|
|
|
|
|
this.resizeProps = {
|
|
|
|
onStopResize: this.onStopResize.bind(this),
|
|
|
|
onResize: this.onResize.bind(this)
|
2018-05-29 15:34:50 +01:00
|
|
|
};
|
2018-05-27 09:37:46 +01:00
|
|
|
|
|
|
|
this.state = {
|
|
|
|
resizing: false
|
2018-05-29 15:34:50 +01:00
|
|
|
};
|
2019-09-18 17:46:19 +02:00
|
|
|
|
|
|
|
this.containerRef = React.createRef();
|
2019-10-16 13:53:16 +02:00
|
|
|
this.editorRef = React.createRef();
|
2018-05-27 09:37:46 +01:00
|
|
|
}
|
|
|
|
onResize() {
|
|
|
|
this.setState({ resizing: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
onStopResize() {
|
|
|
|
this.setState({ resizing: false });
|
|
|
|
}
|
|
|
|
|
2018-04-06 14:51:52 +01:00
|
|
|
componentDidMount() {
|
|
|
|
const {
|
2018-05-24 19:45:38 +01:00
|
|
|
data: {
|
2019-06-20 10:49:53 +02:00
|
|
|
challengeNode: { title }
|
|
|
|
}
|
2018-04-06 14:51:52 +01:00
|
|
|
} = this.props;
|
2019-06-20 10:49:53 +02:00
|
|
|
this.initializeComponent(title);
|
2018-04-06 14:51:52 +01:00
|
|
|
}
|
|
|
|
|
2018-04-06 15:45:49 +01:00
|
|
|
componentDidUpdate(prevProps) {
|
2018-09-30 11:37:19 +01:00
|
|
|
const {
|
|
|
|
data: {
|
|
|
|
challengeNode: { title: prevTitle }
|
|
|
|
}
|
|
|
|
} = prevProps;
|
2019-06-20 10:49:53 +02:00
|
|
|
const {
|
|
|
|
data: {
|
|
|
|
challengeNode: { title: currentTitle }
|
|
|
|
}
|
|
|
|
} = this.props;
|
|
|
|
if (prevTitle !== currentTitle) {
|
|
|
|
this.initializeComponent(currentTitle);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
initializeComponent(title) {
|
2018-04-06 15:45:49 +01:00
|
|
|
const {
|
2018-05-09 12:40:09 +01:00
|
|
|
challengeMounted,
|
2018-04-06 15:45:49 +01:00
|
|
|
createFiles,
|
2019-06-20 10:04:36 +02:00
|
|
|
initConsole,
|
2018-04-06 15:45:49 +01:00
|
|
|
initTests,
|
|
|
|
updateChallengeMeta,
|
|
|
|
data: {
|
2018-05-24 19:45:38 +01:00
|
|
|
challengeNode: {
|
|
|
|
files,
|
|
|
|
fields: { tests },
|
|
|
|
challengeType
|
|
|
|
}
|
2018-04-06 15:45:49 +01:00
|
|
|
},
|
2018-09-11 16:19:11 +03:00
|
|
|
pageContext: { challengeMeta }
|
2018-04-06 15:45:49 +01:00
|
|
|
} = this.props;
|
2019-06-20 10:49:53 +02:00
|
|
|
initConsole('');
|
|
|
|
createFiles(files);
|
|
|
|
initTests(tests);
|
|
|
|
updateChallengeMeta({ ...challengeMeta, title, challengeType });
|
|
|
|
challengeMounted(challengeMeta.id);
|
2018-04-06 15:45:49 +01:00
|
|
|
}
|
|
|
|
|
2019-02-06 14:30:54 +03:00
|
|
|
componentWillUnmount() {
|
2020-02-03 15:22:49 +02:00
|
|
|
const { createFiles, cancelTests } = this.props;
|
2019-02-06 14:30:54 +03:00
|
|
|
createFiles({});
|
2020-02-03 15:22:49 +02:00
|
|
|
cancelTests();
|
2019-02-06 14:30:54 +03:00
|
|
|
}
|
|
|
|
|
2018-12-11 23:05:15 +02:00
|
|
|
getChallenge = () => this.props.data.challengeNode;
|
|
|
|
|
2018-12-07 14:08:32 +02:00
|
|
|
getBlockNameTitle() {
|
2018-04-06 14:51:52 +01:00
|
|
|
const {
|
2018-12-11 23:05:15 +02:00
|
|
|
fields: { blockName },
|
|
|
|
title
|
|
|
|
} = this.getChallenge();
|
2018-12-07 14:08:32 +02:00
|
|
|
return `${blockName}: ${title}`;
|
|
|
|
}
|
|
|
|
|
2018-12-11 23:05:15 +02:00
|
|
|
getVideoUrl = () => this.getChallenge().videoUrl;
|
|
|
|
|
2018-12-07 14:08:32 +02:00
|
|
|
getChallengeFile() {
|
|
|
|
const { files } = this.props;
|
|
|
|
return first(Object.keys(files).map(key => files[key]));
|
|
|
|
}
|
|
|
|
|
|
|
|
hasPreview() {
|
2018-12-11 23:05:15 +02:00
|
|
|
const { challengeType } = this.getChallenge();
|
2018-12-07 14:08:32 +02:00
|
|
|
return (
|
|
|
|
challengeType === challengeTypes.html ||
|
|
|
|
challengeType === challengeTypes.modern
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderInstructionsPanel({ showToolPanel }) {
|
|
|
|
const {
|
2018-12-11 23:05:15 +02:00
|
|
|
fields: { blockName },
|
|
|
|
description,
|
2019-01-23 12:24:05 +03:00
|
|
|
instructions
|
2018-12-11 23:05:15 +02:00
|
|
|
} = this.getChallenge();
|
|
|
|
|
2019-08-28 21:55:56 +05:30
|
|
|
const { forumTopicId, title } = this.getChallenge();
|
2018-12-07 14:08:32 +02:00
|
|
|
return (
|
|
|
|
<SidePanel
|
|
|
|
className='full-height'
|
|
|
|
description={description}
|
2019-08-28 21:55:56 +05:30
|
|
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
2018-12-07 14:08:32 +02:00
|
|
|
instructions={instructions}
|
|
|
|
section={dasherize(blockName)}
|
|
|
|
showToolPanel={showToolPanel}
|
|
|
|
title={this.getBlockNameTitle()}
|
2018-12-11 23:05:15 +02:00
|
|
|
videoUrl={this.getVideoUrl()}
|
2018-12-07 14:08:32 +02:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderEditor() {
|
2019-09-18 17:46:19 +02:00
|
|
|
const { files } = this.props;
|
2019-09-17 15:32:23 +02:00
|
|
|
|
2019-01-23 12:24:05 +03:00
|
|
|
return (
|
2020-05-05 07:48:51 -05:00
|
|
|
files && (
|
2019-09-17 15:32:23 +02:00
|
|
|
<Editor
|
2020-05-05 07:48:51 -05:00
|
|
|
challengeFiles={files}
|
2019-09-18 17:46:19 +02:00
|
|
|
containerRef={this.containerRef}
|
2019-10-16 13:53:16 +02:00
|
|
|
ref={this.editorRef}
|
2019-09-17 15:32:23 +02:00
|
|
|
/>
|
|
|
|
)
|
2018-12-07 14:08:32 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderTestOutput() {
|
|
|
|
const { output } = this.props;
|
|
|
|
return (
|
|
|
|
<Output
|
2019-01-23 12:24:05 +03:00
|
|
|
defaultOutput={`
|
2018-05-18 14:54:21 +01:00
|
|
|
/**
|
2018-06-01 03:36:42 +05:30
|
|
|
* Your test output will go here.
|
2018-05-18 14:54:21 +01:00
|
|
|
*/
|
|
|
|
`}
|
2019-01-27 17:15:21 +03:00
|
|
|
output={output}
|
2018-12-07 14:08:32 +02:00
|
|
|
/>
|
2018-09-30 11:37:19 +01:00
|
|
|
);
|
2018-12-07 14:08:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
renderPreview() {
|
2018-04-06 14:51:52 +01:00
|
|
|
return (
|
2019-01-23 12:24:05 +03:00
|
|
|
<Preview className='full-height' disableIframe={this.state.resizing} />
|
2018-12-07 14:08:32 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2019-11-26 22:14:44 +05:30
|
|
|
const {
|
|
|
|
fields: { blockName },
|
|
|
|
forumTopicId,
|
|
|
|
title
|
|
|
|
} = this.getChallenge();
|
2019-09-18 17:46:19 +02:00
|
|
|
const {
|
|
|
|
executeChallenge,
|
|
|
|
pageContext: {
|
|
|
|
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
|
|
|
}
|
|
|
|
} = this.props;
|
2018-12-07 14:08:32 +02:00
|
|
|
return (
|
2019-09-18 17:46:19 +02:00
|
|
|
<Hotkeys
|
2019-10-16 13:53:16 +02:00
|
|
|
editorRef={this.editorRef}
|
2019-09-18 17:46:19 +02:00
|
|
|
executeChallenge={executeChallenge}
|
|
|
|
innerRef={this.containerRef}
|
|
|
|
introPath={introPath}
|
|
|
|
nextChallengePath={nextChallengePath}
|
|
|
|
prevChallengePath={prevChallengePath}
|
|
|
|
>
|
|
|
|
<LearnLayout>
|
|
|
|
<Helmet
|
|
|
|
title={`Learn ${this.getBlockNameTitle()} | freeCodeCamp.org`}
|
2019-05-14 17:37:13 +03:00
|
|
|
/>
|
2019-09-18 17:46:19 +02:00
|
|
|
<Media maxWidth={MAX_MOBILE_WIDTH}>
|
|
|
|
<MobileLayout
|
|
|
|
editor={this.renderEditor()}
|
|
|
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
|
|
|
hasPreview={this.hasPreview()}
|
|
|
|
instructions={this.renderInstructionsPanel({
|
|
|
|
showToolPanel: false
|
|
|
|
})}
|
|
|
|
preview={this.renderPreview()}
|
|
|
|
testOutput={this.renderTestOutput()}
|
|
|
|
videoUrl={this.getVideoUrl()}
|
|
|
|
/>
|
|
|
|
</Media>
|
|
|
|
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
|
|
|
<DesktopLayout
|
|
|
|
challengeFile={this.getChallengeFile()}
|
|
|
|
editor={this.renderEditor()}
|
|
|
|
hasPreview={this.hasPreview()}
|
|
|
|
instructions={this.renderInstructionsPanel({
|
|
|
|
showToolPanel: true
|
|
|
|
})}
|
|
|
|
preview={this.renderPreview()}
|
|
|
|
resizeProps={this.resizeProps}
|
|
|
|
testOutput={this.renderTestOutput()}
|
|
|
|
/>
|
|
|
|
</Media>
|
2019-11-26 22:14:44 +05:30
|
|
|
<CompletionModal blockName={blockName} />
|
2019-09-18 17:46:19 +02:00
|
|
|
<HelpModal />
|
|
|
|
<VideoModal videoUrl={this.getVideoUrl()} />
|
|
|
|
<ResetModal />
|
|
|
|
</LearnLayout>
|
|
|
|
</Hotkeys>
|
2018-04-06 14:51:52 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ShowClassic.displayName = 'ShowClassic';
|
|
|
|
ShowClassic.propTypes = propTypes;
|
|
|
|
|
2018-09-30 11:37:19 +01:00
|
|
|
export default connect(
|
|
|
|
mapStateToProps,
|
|
|
|
mapDispatchToProps
|
|
|
|
)(ShowClassic);
|
2018-04-06 14:51:52 +01:00
|
|
|
|
2020-06-25 17:25:30 +02:00
|
|
|
// TODO: handle jsx (not sure why it doesn't get an editableRegion)
|
2018-04-06 14:51:52 +01:00
|
|
|
export const query = graphql`
|
|
|
|
query ClassicChallenge($slug: String!) {
|
|
|
|
challengeNode(fields: { slug: { eq: $slug } }) {
|
|
|
|
title
|
|
|
|
description
|
2018-10-05 10:17:34 +01:00
|
|
|
instructions
|
2018-04-06 14:51:52 +01:00
|
|
|
challengeType
|
2018-08-06 09:25:50 -04:00
|
|
|
videoUrl
|
2019-08-02 01:13:18 +05:30
|
|
|
forumTopicId
|
2018-04-06 14:51:52 +01:00
|
|
|
fields {
|
2018-06-28 18:53:47 +01:00
|
|
|
slug
|
2018-04-06 14:51:52 +01:00
|
|
|
blockName
|
|
|
|
tests {
|
|
|
|
text
|
|
|
|
testString
|
|
|
|
}
|
|
|
|
}
|
2018-04-11 14:43:23 +01:00
|
|
|
required {
|
|
|
|
link
|
|
|
|
src
|
|
|
|
}
|
2018-04-06 14:51:52 +01:00
|
|
|
files {
|
2020-05-05 07:48:51 -05:00
|
|
|
indexcss {
|
|
|
|
key
|
|
|
|
ext
|
|
|
|
name
|
|
|
|
contents
|
|
|
|
head
|
|
|
|
tail
|
2020-06-25 17:25:30 +02:00
|
|
|
editableRegionBoundaries
|
2020-05-05 07:48:51 -05:00
|
|
|
}
|
2018-04-06 14:51:52 +01:00
|
|
|
indexhtml {
|
|
|
|
key
|
|
|
|
ext
|
|
|
|
name
|
|
|
|
contents
|
|
|
|
head
|
|
|
|
tail
|
2020-06-25 17:25:30 +02:00
|
|
|
editableRegionBoundaries
|
2018-04-06 14:51:52 +01:00
|
|
|
}
|
|
|
|
indexjs {
|
|
|
|
key
|
|
|
|
ext
|
|
|
|
name
|
|
|
|
contents
|
|
|
|
head
|
|
|
|
tail
|
2020-06-25 17:25:30 +02:00
|
|
|
editableRegionBoundaries
|
2018-04-06 14:51:52 +01:00
|
|
|
}
|
2018-04-11 14:43:23 +01:00
|
|
|
indexjsx {
|
|
|
|
key
|
|
|
|
ext
|
|
|
|
name
|
|
|
|
contents
|
|
|
|
head
|
|
|
|
tail
|
|
|
|
}
|
2018-04-06 14:51:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|