Files
freeCodeCamp/client/src/templates/Challenges/classic/Show.js

387 lines
8.9 KiB
JavaScript
Raw Normal View History

import React, { Component } from 'react';
2018-04-06 14:51:52 +01:00
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
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';
import { first } from 'lodash';
import Media from 'react-responsive';
2018-04-06 14:51:52 +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';
import Output from '../components/Output';
2018-04-06 14:51:52 +01:00
import CompletionModal from '../components/CompletionModal';
import HelpModal from '../components/HelpModal';
import VideoModal from '../components/VideoModal';
import ResetModal from '../components/ResetModal';
import MobileLayout from './MobileLayout';
import DesktopLayout from './DesktopLayout';
import Hotkeys from '../components/Hotkeys';
2018-04-06 14:51:52 +01:00
import { getGuideUrl } from '../utils';
import { challengeTypes } from '../../../../utils/challengeTypes';
import { ChallengeNode } from '../../../redux/propTypes';
import { dasherize } from '../../../../../utils/slugs';
2018-04-06 14:51:52 +01:00
import {
createFiles,
challengeFilesSelector,
challengeTestsSelector,
initConsole,
2018-04-06 14:51:52 +01:00
initTests,
updateChallengeMeta,
challengeMounted,
consoleOutputSelector,
executeChallenge,
cancelTests
} 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
const mapStateToProps = createStructuredSelector({
files: challengeFilesSelector,
tests: challengeTestsSelector,
output: consoleOutputSelector
});
2018-04-06 14:51:52 +01:00
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
createFiles,
initConsole,
initTests,
updateChallengeMeta,
challengeMounted,
executeChallenge,
cancelTests
},
dispatch
);
2018-04-06 14:51:52 +01:00
const propTypes = {
cancelTests: PropTypes.func.isRequired,
challengeMounted: PropTypes.func.isRequired,
2018-04-06 14:51:52 +01:00
createFiles: PropTypes.func.isRequired,
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
executeChallenge: PropTypes.func.isRequired,
2018-04-06 14:51:52 +01:00
files: PropTypes.shape({
key: PropTypes.string
}),
initConsole: PropTypes.func.isRequired,
2018-04-06 14:51:52 +01:00
initTests: PropTypes.func.isRequired,
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
}),
tests: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
testString: PropTypes.string
})
),
updateChallengeMeta: PropTypes.func.isRequired
2018-04-06 14:51:52 +01:00
};
const MAX_MOBILE_WIDTH = 767;
class ShowClassic extends Component {
constructor() {
super();
this.resizeProps = {
onStopResize: this.onStopResize.bind(this),
onResize: this.onResize.bind(this)
};
this.state = {
resizing: false
};
this.containerRef = React.createRef();
this.editorRef = React.createRef();
}
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: {
challengeNode: { title }
}
2018-04-06 14:51:52 +01:00
} = this.props;
this.initializeComponent(title);
2018-04-06 14:51:52 +01:00
}
2018-04-06 15:45:49 +01:00
componentDidUpdate(prevProps) {
const {
data: {
challengeNode: { title: prevTitle }
}
} = prevProps;
const {
data: {
challengeNode: { title: currentTitle }
}
} = this.props;
if (prevTitle !== currentTitle) {
this.initializeComponent(currentTitle);
}
}
initializeComponent(title) {
2018-04-06 15:45:49 +01:00
const {
challengeMounted,
2018-04-06 15:45:49 +01:00
createFiles,
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;
initConsole('');
createFiles(files);
initTests(tests);
updateChallengeMeta({ ...challengeMeta, title, challengeType });
challengeMounted(challengeMeta.id);
2018-04-06 15:45:49 +01:00
}
componentWillUnmount() {
const { createFiles, cancelTests } = this.props;
createFiles({});
cancelTests();
}
getChallenge = () => this.props.data.challengeNode;
getBlockNameTitle() {
2018-04-06 14:51:52 +01:00
const {
fields: { blockName },
title
} = this.getChallenge();
return `${blockName}: ${title}`;
}
getVideoUrl = () => this.getChallenge().videoUrl;
getChallengeFile() {
const { files } = this.props;
return first(Object.keys(files).map(key => files[key]));
}
hasPreview() {
const { challengeType } = this.getChallenge();
return (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern
);
}
renderInstructionsPanel({ showToolPanel }) {
const {
fields: { blockName },
description,
2019-01-23 12:24:05 +03:00
instructions
} = this.getChallenge();
const { forumTopicId, title } = this.getChallenge();
return (
<SidePanel
className='full-height'
description={description}
guideUrl={getGuideUrl({ forumTopicId, title })}
instructions={instructions}
section={dasherize(blockName)}
showToolPanel={showToolPanel}
title={this.getBlockNameTitle()}
videoUrl={this.getVideoUrl()}
/>
);
}
renderEditor() {
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}
containerRef={this.containerRef}
ref={this.editorRef}
2019-09-17 15:32:23 +02:00
/>
)
);
}
renderTestOutput() {
const { output } = this.props;
return (
<Output
2019-01-23 12:24:05 +03:00
defaultOutput={`
/**
* Your test output will go here.
*/
`}
output={output}
/>
);
}
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} />
);
}
render() {
const {
fields: { blockName },
forumTopicId,
title
} = this.getChallenge();
const {
executeChallenge,
pageContext: {
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
}
} = this.props;
return (
<Hotkeys
editorRef={this.editorRef}
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
/>
<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>
<CompletionModal blockName={blockName} />
<HelpModal />
<VideoModal videoUrl={this.getVideoUrl()} />
<ResetModal />
</LearnLayout>
</Hotkeys>
2018-04-06 14:51:52 +01:00
);
}
}
ShowClassic.displayName = 'ShowClassic';
ShowClassic.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShowClassic);
2018-04-06 14:51:52 +01: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
instructions
2018-04-06 14:51:52 +01:00
challengeType
videoUrl
forumTopicId
2018-04-06 14:51:52 +01:00
fields {
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
editableRegionBoundaries
2020-05-05 07:48:51 -05:00
}
2018-04-06 14:51:52 +01:00
indexhtml {
key
ext
name
contents
head
tail
editableRegionBoundaries
2018-04-06 14:51:52 +01:00
}
indexjs {
key
ext
name
contents
head
tail
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
}
}
}
`;