fix(learn): created components for mobile/desktop layouts (#17467)

This commit is contained in:
Ivan Nikolaievskyi
2018-12-11 23:05:15 +02:00
committed by Stuart Taylor
parent d4b07f47ab
commit 8a60b19b6c
4 changed files with 225 additions and 163 deletions

View File

@ -0,0 +1,78 @@
import React, { Component, Fragment } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import PropTypes from 'prop-types';
const propTypes = {
resizeProps: PropTypes.shape({
onStopResize: PropTypes.func,
onResize: PropTypes.func
}),
instructions: PropTypes.element,
challengeFile: PropTypes.shape({
key: PropTypes.string
}),
editor: PropTypes.element,
testOutput: PropTypes.element,
hasPreview: PropTypes.bool,
preview: PropTypes.element
};
class DesktopLayout extends Component {
render() {
const {
resizeProps,
instructions,
challengeFile,
editor,
testOutput,
hasPreview,
preview
} = this.props;
return (
<ReflexContainer orientation='vertical'>
<ReflexElement flex={1} {...resizeProps}>
{instructions}
</ReflexElement>
<ReflexSplitter propagate={true} {...resizeProps} />
<ReflexElement flex={1} {...resizeProps}>
{challengeFile && (
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
<ReflexElement
flex={1}
propagateDimensions={true}
renderOnResize={true}
renderOnResizeRate={20}
{...resizeProps}
>
{editor}
</ReflexElement>
<ReflexSplitter propagate={true} {...resizeProps} />
<ReflexElement
flex={0.25}
propagateDimensions={true}
renderOnResize={true}
renderOnResizeRate={20}
{...resizeProps}
>
{testOutput}
</ReflexElement>
</ReflexContainer>
)}
</ReflexElement>
{hasPreview && (
<Fragment>
<ReflexSplitter propagate={true} {...resizeProps} />
<ReflexElement flex={0.7} {...resizeProps}>
{preview}
</ReflexElement>
</Fragment>
)}
</ReflexContainer>
);
}
}
DesktopLayout.displayName = 'DesktopLayout';
DesktopLayout.propTypes = propTypes;
export default DesktopLayout;

View File

@ -0,0 +1,98 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { TabPane, Tabs } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import ToolPanel from '../components/Tool-Panel';
import { createStructuredSelector } from 'reselect';
import {
currentTabSelector,
moveToTab,
} from '../redux';
import { bindActionCreators } from 'redux';
const mapStateToProps = createStructuredSelector({
currentTab: currentTabSelector
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
moveToTab,
},
dispatch
);
const propTypes = {
moveToTab: PropTypes.func,
currentTab: PropTypes.number,
instructions: PropTypes.element,
editor: PropTypes.element,
testOutput: PropTypes.element,
hasPreview: PropTypes.bool,
preview: PropTypes.element,
guideUrl: PropTypes.string,
videoUrl: PropTypes.string
};
class MobileLayout extends Component {
render() {
const {
currentTab,
moveToTab,
instructions,
editor,
testOutput,
hasPreview,
preview,
guideUrl,
videoUrl
} = this.props;
const editorTabPaneProps = {
mountOnEnter: true,
unmountOnExit: true
};
return (
<Fragment>
<Tabs
activeKey={currentTab}
defaultActiveKey={1}
id='challege-page-tabs'
onSelect={(key) => moveToTab(key)}
>
<TabPane eventKey={1} title='Instructions'>
{ instructions }
</TabPane>
<TabPane eventKey={2} title='Code' {...editorTabPaneProps}>
<div className='challege-edittor-wrapper'>
{editor}
</div>
</TabPane>
<TabPane eventKey={3} title='Tests' {...editorTabPaneProps}>
<div className='challege-edittor-wrapper'>
{testOutput}
</div>
</TabPane>
{hasPreview && (
<TabPane eventKey={4} title='Preview'>
{preview}
</TabPane>
)}
</Tabs>
<ToolPanel
guideUrl={guideUrl}
isMobile={true}
videoUrl={videoUrl}
/>
</Fragment>
);
}
}
MobileLayout.displayName = 'MobileLayout';
MobileLayout.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(MobileLayout);

View File

@ -1,13 +1,11 @@
import React, { Component, Fragment } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { createSelector } from 'reselect'; import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import { first } from 'lodash'; import { first } from 'lodash';
import { Tabs, TabPane } from '@freecodecamp/react-bootstrap';
import Media from 'react-media'; import Media from 'react-media';
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
@ -19,7 +17,8 @@ import CompletionModal from '../components/CompletionModal';
import HelpModal from '../components/HelpModal'; import HelpModal from '../components/HelpModal';
import VideoModal from '../components/VideoModal'; import VideoModal from '../components/VideoModal';
import ResetModal from '../components/ResetModal'; import ResetModal from '../components/ResetModal';
import ToolPanel from '../components/Tool-Panel'; import MobileLayout from './MobileLayout';
import DesktopLayout from './DesktopLayout';
import { randomCompliment } from '../utils/get-words'; import { randomCompliment } from '../utils/get-words';
import { createGuideUrl } from '../utils'; import { createGuideUrl } from '../utils';
@ -30,13 +29,11 @@ import {
createFiles, createFiles,
challengeFilesSelector, challengeFilesSelector,
challengeTestsSelector, challengeTestsSelector,
currentTabSelector,
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
challengeMounted, challengeMounted,
updateSuccessMessage, updateSuccessMessage,
consoleOutputSelector, consoleOutputSelector
moveToTab
} from '../redux'; } from '../redux';
import './classic.css'; import './classic.css';
@ -44,23 +41,15 @@ import '../components/test-frame.css';
import decodeHTMLEntities from '../../../../utils/decodeHTMLEntities'; import decodeHTMLEntities from '../../../../utils/decodeHTMLEntities';
const mapStateToProps = createSelector( const mapStateToProps = createStructuredSelector({
challengeFilesSelector, files: challengeFilesSelector,
challengeTestsSelector, tests: challengeTestsSelector,
consoleOutputSelector, output: consoleOutputSelector
currentTabSelector, });
(files, tests, output, currentTab) => ({
files,
tests,
output,
currentTab
})
);
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ {
moveToTab,
createFiles, createFiles,
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
@ -73,7 +62,6 @@ const mapDispatchToProps = dispatch =>
const propTypes = { const propTypes = {
challengeMounted: PropTypes.func.isRequired, challengeMounted: PropTypes.func.isRequired,
createFiles: PropTypes.func.isRequired, createFiles: PropTypes.func.isRequired,
currentTab: PropTypes.number,
data: PropTypes.shape({ data: PropTypes.shape({
challengeNode: ChallengeNode challengeNode: ChallengeNode
}), }),
@ -81,7 +69,6 @@ const propTypes = {
key: PropTypes.string key: PropTypes.string
}), }),
initTests: PropTypes.func.isRequired, initTests: PropTypes.func.isRequired,
moveToTab: PropTypes.func.isRequired,
output: PropTypes.string, output: PropTypes.string,
pageContext: PropTypes.shape({ pageContext: PropTypes.shape({
challengeMeta: PropTypes.shape({ challengeMeta: PropTypes.shape({
@ -180,42 +167,30 @@ class ShowClassic extends Component {
} }
} }
getChallenge = () => this.props.data.challengeNode;
getBlockNameTitle() { getBlockNameTitle() {
const { const {
data: { fields: { blockName },
challengeNode: { title
fields: { blockName }, } = this.getChallenge();
title
}
}
} = this.props;
return `${blockName}: ${title}`; return `${blockName}: ${title}`;
} }
getGuideUrl() { getGuideUrl() {
const { const {fields: { slug }} = this.getChallenge();
data: {
challengeNode: {
fields: { slug }
}
}
} = this.props;
return createGuideUrl(slug); return createGuideUrl(slug);
} }
getVideoUrl = () => this.getChallenge().videoUrl;
getChallengeFile() { getChallengeFile() {
const { files } = this.props; const { files } = this.props;
return first(Object.keys(files).map(key => files[key])); return first(Object.keys(files).map(key => files[key]));
} }
hasPreview() { hasPreview() {
const { const { challengeType } = this.getChallenge();
data: {
challengeNode: {
challengeType
}
}
} = this.props;
return ( return (
challengeType === challengeTypes.html || challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern challengeType === challengeTypes.modern
@ -224,15 +199,11 @@ class ShowClassic extends Component {
renderInstructionsPanel({ showToolPanel }) { renderInstructionsPanel({ showToolPanel }) {
const { const {
data: { fields: { blockName },
challengeNode: { description,
fields: { blockName }, instructions,
description, } = this.getChallenge();
instructions,
videoUrl
}
}
} = this.props;
return ( return (
<SidePanel <SidePanel
className='full-height' className='full-height'
@ -242,7 +213,7 @@ class ShowClassic extends Component {
section={dasherize(blockName)} section={dasherize(blockName)}
showToolPanel={showToolPanel} showToolPanel={showToolPanel}
title={this.getBlockNameTitle()} title={this.getBlockNameTitle()}
videoUrl={videoUrl} videoUrl={this.getVideoUrl()}
/> />
); );
} }
@ -278,112 +249,7 @@ class ShowClassic extends Component {
); );
} }
renderDesktopLayout() {
const challengeFile = this.getChallengeFile();
return (
<ReflexContainer orientation='vertical'>
<ReflexElement flex={1} {...this.resizeProps}>
{this.renderInstructionsPanel({ showToolPanel: true })}
</ReflexElement>
<ReflexSplitter propagate={true} {...this.resizeProps} />
<ReflexElement flex={1} {...this.resizeProps}>
{challengeFile && (
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
<ReflexElement
flex={1}
propagateDimensions={true}
renderOnResize={true}
renderOnResizeRate={20}
{...this.resizeProps}
>
{this.renderEditor()}
</ReflexElement>
<ReflexSplitter propagate={true} {...this.resizeProps} />
<ReflexElement
flex={0.25}
propagateDimensions={true}
renderOnResize={true}
renderOnResizeRate={20}
{...this.resizeProps}
>
{this.renderTestOutput()}
</ReflexElement>
</ReflexContainer>
)}
</ReflexElement>
{this.hasPreview() && (
<Fragment>
<ReflexSplitter propagate={true} {...this.resizeProps} />
<ReflexElement flex={0.7} {...this.resizeProps}>
{this.renderPreview()}
</ReflexElement>
</Fragment>
)}
</ReflexContainer>
);
}
renderMobileLayout() {
const {
data: {
challengeNode: {
videoUrl
}
},
currentTab,
moveToTab
} = this.props;
const editorTabPaneProps = {
mountOnEnter: true,
unmountOnExit: true
};
return (
<Fragment>
<Tabs
activeKey={currentTab}
defaultActiveKey={1}
id='challege-page-tabs'
onSelect={(key) => moveToTab(key)}
>
<TabPane eventKey={1} title='Instructions'>
{ this.renderInstructionsPanel({ showToolPanel: false }) }
</TabPane>
<TabPane eventKey={2} title='Code' {...editorTabPaneProps}>
<div className='challege-edittor-wrapper'>
{this.renderEditor()}
</div>
</TabPane>
<TabPane eventKey={3} title='Tests' {...editorTabPaneProps}>
<div className='challege-edittor-wrapper'>
{this.renderTestOutput()}
</div>
</TabPane>
{this.hasPreview() && (
<TabPane eventKey={4} title='Preview'>
{this.renderPreview()}
</TabPane>
)}
</Tabs>
<ToolPanel
guideUrl={this.getGuideUrl()}
isMobile={true}
videoUrl={videoUrl}
/>
</Fragment>
);
}
render() { render() {
const {
data: {
challengeNode: {
videoUrl
}
}
} = this.props;
return ( return (
<LearnLayout> <LearnLayout>
<Helmet <Helmet
@ -392,13 +258,33 @@ class ShowClassic extends Component {
<Media query={{ maxWidth: MAX_MOBILE_WIDTH }}> <Media query={{ maxWidth: MAX_MOBILE_WIDTH }}>
{matches => {matches =>
matches matches
? this.renderMobileLayout() ? (
: this.renderDesktopLayout() <MobileLayout
instructions={this.renderInstructionsPanel({ showToolPanel: false })}
editor={this.renderEditor()}
testOutput={this.renderTestOutput()}
hasPreview={this.hasPreview()}
preview={this.renderPreview()}
guideUrl={this.getGuideUrl()}
videoUrl={this.getVideoUrl()}
/>
)
: (
<DesktopLayout
instructions={this.renderInstructionsPanel({ showToolPanel: true })}
editor={this.renderEditor()}
testOutput={this.renderTestOutput()}
hasPreview={this.hasPreview()}
preview={this.renderPreview()}
resizeProps={this.resizeProps}
challengeFile={this.getChallengeFile()}
/>
)
} }
</Media> </Media>
<CompletionModal /> <CompletionModal />
<HelpModal /> <HelpModal />
<VideoModal videoUrl={videoUrl} /> <VideoModal videoUrl={this.getVideoUrl()} />
<ResetModal /> <ResetModal />
</LearnLayout> </LearnLayout>
); );

View File

@ -146,7 +146,7 @@ export const noStoredCodeFound = createAction(types.noStoredCodeFound);
export const closeModal = createAction(types.closeModal); export const closeModal = createAction(types.closeModal);
export const openModal = createAction(types.openModal); export const openModal = createAction(types.openModal);
export const previewMounted = createAction(types.challengeMounted); export const previewMounted = createAction(types.previewMounted);
export const challengeMounted = createAction(types.challengeMounted); export const challengeMounted = createAction(types.challengeMounted);
export const checkChallenge = createAction(types.checkChallenge); export const checkChallenge = createAction(types.checkChallenge);
export const executeChallenge = createAction(types.executeChallenge); export const executeChallenge = createAction(types.executeChallenge);