import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { createStructuredSelector } from 'reselect'; import { connect } from 'react-redux'; import Helmet from 'react-helmet'; import { graphql } from 'gatsby'; import Media from 'react-responsive'; import { withTranslation } from 'react-i18next'; import LearnLayout from '../../../components/layouts/Learn'; import MultifileEditor from './MultifileEditor'; import Preview from '../components/Preview'; import SidePanel from '../components/Side-Panel'; import Output from '../components/Output'; 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'; import { getGuideUrl } from '../utils'; import { challengeTypes } from '../../../../utils/challengeTypes'; import { ChallengeNode } from '../../../redux/propTypes'; import { createFiles, challengeFilesSelector, challengeTestsSelector, initConsole, initTests, updateChallengeMeta, challengeMounted, consoleOutputSelector, executeChallenge, cancelTests } from '../redux'; import './classic.css'; import '../components/test-frame.css'; const mapStateToProps = createStructuredSelector({ files: challengeFilesSelector, tests: challengeTestsSelector, output: consoleOutputSelector }); const mapDispatchToProps = dispatch => bindActionCreators( { createFiles, initConsole, initTests, updateChallengeMeta, challengeMounted, executeChallenge, cancelTests }, dispatch ); const propTypes = { cancelTests: PropTypes.func.isRequired, challengeMounted: PropTypes.func.isRequired, createFiles: PropTypes.func.isRequired, data: PropTypes.shape({ challengeNode: ChallengeNode }), executeChallenge: PropTypes.func.isRequired, files: PropTypes.shape({ key: PropTypes.string }), initConsole: PropTypes.func.isRequired, initTests: PropTypes.func.isRequired, output: PropTypes.arrayOf(PropTypes.string), pageContext: PropTypes.shape({ challengeMeta: PropTypes.shape({ id: PropTypes.string, nextChallengePath: PropTypes.string, prevChallengePath: PropTypes.string }) }), t: PropTypes.func.isRequired, tests: PropTypes.arrayOf( PropTypes.shape({ text: PropTypes.string, testString: PropTypes.string }) ), updateChallengeMeta: PropTypes.func.isRequired }; 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 }); } componentDidMount() { const { data: { challengeNode: { title } } } = this.props; this.initializeComponent(title); } componentDidUpdate(prevProps) { const { data: { challengeNode: { title: prevTitle, fields: { tests: prevTests } } } } = prevProps; const { data: { challengeNode: { title: currentTitle, fields: { tests: currTests } } } } = this.props; if (prevTitle !== currentTitle || prevTests !== currTests) { this.initializeComponent(currentTitle); } } initializeComponent(title) { const { challengeMounted, createFiles, initConsole, initTests, updateChallengeMeta, data: { challengeNode: { files, fields: { tests }, challengeType, removeComments, helpCategory } }, pageContext: { challengeMeta } } = this.props; initConsole(''); createFiles(files); initTests(tests); updateChallengeMeta({ ...challengeMeta, title, removeComments, challengeType, helpCategory }); challengeMounted(challengeMeta.id); } componentWillUnmount() { const { createFiles, cancelTests } = this.props; createFiles({}); cancelTests(); } getChallenge = () => this.props.data.challengeNode; getBlockNameTitle() { const { fields: { blockName }, title } = this.getChallenge(); return `${blockName}: ${title}`; } getVideoUrl = () => this.getChallenge().videoUrl; hasPreview() { const { challengeType } = this.getChallenge(); return ( challengeType === challengeTypes.html || challengeType === challengeTypes.modern ); } renderInstructionsPanel({ showToolPanel }) { const { block, description, instructions, superBlock, translationPending } = this.getChallenge(); const { forumTopicId, title } = this.getChallenge(); return ( ); } renderEditor() { const { files } = this.props; const { description } = this.getChallenge(); return ( files && ( ) ); } renderTestOutput() { const { output, t } = this.props; return ( ); } renderPreview() { return ( ); } hasEditableBoundries() { const { files } = this.props; return Object.values(files).some( file => file.editableRegionBoundaries && file.editableRegionBoundaries.length === 2 ); } render() { const { block, fields: { blockName }, forumTopicId, superBlock, title } = this.getChallenge(); const { executeChallenge, pageContext: { challengeMeta: { nextChallengePath, prevChallengePath } }, files, t } = this.props; return ( ); } } ShowClassic.displayName = 'ShowClassic'; ShowClassic.propTypes = propTypes; export default connect( mapStateToProps, mapDispatchToProps )(withTranslation()(ShowClassic)); // TODO: handle jsx (not sure why it doesn't get an editableRegion) EDIT: // probably because the dummy challenge didn't include it, so Gatsby couldn't // infer it. export const query = graphql` query ClassicChallenge($slug: String!) { challengeNode(fields: { slug: { eq: $slug } }) { block title description instructions removeComments challengeType helpCategory videoUrl superBlock translationPending forumTopicId fields { blockName slug tests { text testString } } required { link src } files { indexcss { key ext name contents head tail editableRegionBoundaries } indexhtml { key ext name contents head tail editableRegionBoundaries } indexjs { key ext name contents head tail editableRegionBoundaries } indexjsx { key ext name contents head tail editableRegionBoundaries } } } } `;