fix(learn): created components for mobile/desktop layouts (#17467)
This commit is contained in:
committed by
Stuart Taylor
parent
d4b07f47ab
commit
8a60b19b6c
78
client/src/templates/Challenges/classic/DesktopLayout.js
Normal file
78
client/src/templates/Challenges/classic/DesktopLayout.js
Normal 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;
|
98
client/src/templates/Challenges/classic/MobileLayout.js
Normal file
98
client/src/templates/Challenges/classic/MobileLayout.js
Normal 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);
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
Reference in New Issue
Block a user