fix(learn): implemented curriculum layout for mobile devices (#17467)
remove useless changes
This commit is contained in:
committed by
Stuart Taylor
parent
37d98f1123
commit
d4b07f47ab
@ -25,6 +25,13 @@
|
|||||||
padding: 5px 10px 5px 10px;
|
padding: 5px 10px 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
#learn-app-wrapper {
|
||||||
|
right: -2em;
|
||||||
|
padding: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.reflex-layout.reflex-container.vertical {
|
.reflex-layout.reflex-container.vertical {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 18px;
|
margin: 0 18px;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } 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 { createSelector } from 'reselect';
|
||||||
@ -7,6 +7,8 @@ import Helmet from 'react-helmet';
|
|||||||
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
|
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 LearnLayout from '../../../components/layouts/Learn';
|
import LearnLayout from '../../../components/layouts/Learn';
|
||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
@ -17,6 +19,7 @@ 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 { randomCompliment } from '../utils/get-words';
|
import { randomCompliment } from '../utils/get-words';
|
||||||
import { createGuideUrl } from '../utils';
|
import { createGuideUrl } from '../utils';
|
||||||
@ -27,11 +30,13 @@ 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';
|
||||||
@ -43,16 +48,19 @@ const mapStateToProps = createSelector(
|
|||||||
challengeFilesSelector,
|
challengeFilesSelector,
|
||||||
challengeTestsSelector,
|
challengeTestsSelector,
|
||||||
consoleOutputSelector,
|
consoleOutputSelector,
|
||||||
(files, tests, output) => ({
|
currentTabSelector,
|
||||||
|
(files, tests, output, currentTab) => ({
|
||||||
files,
|
files,
|
||||||
tests,
|
tests,
|
||||||
output
|
output,
|
||||||
|
currentTab
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
|
moveToTab,
|
||||||
createFiles,
|
createFiles,
|
||||||
initTests,
|
initTests,
|
||||||
updateChallengeMeta,
|
updateChallengeMeta,
|
||||||
@ -65,6 +73,7 @@ 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
|
||||||
}),
|
}),
|
||||||
@ -72,6 +81,7 @@ 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({
|
||||||
@ -88,6 +98,8 @@ const propTypes = {
|
|||||||
updateSuccessMessage: PropTypes.func.isRequired
|
updateSuccessMessage: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_MOBILE_WIDTH = 767;
|
||||||
|
|
||||||
class ShowClassic extends Component {
|
class ShowClassic extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -101,7 +113,6 @@ class ShowClassic extends Component {
|
|||||||
resizing: false
|
resizing: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize() {
|
onResize() {
|
||||||
this.setState({ resizing: true });
|
this.setState({ resizing: true });
|
||||||
}
|
}
|
||||||
@ -169,23 +180,114 @@ class ShowClassic extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
getBlockNameTitle() {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
challengeType,
|
fields: { blockName },
|
||||||
fields: { blockName, slug },
|
title
|
||||||
title,
|
}
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
|
return `${blockName}: ${title}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGuideUrl() {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
fields: { slug }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
|
return createGuideUrl(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
getChallengeFile() {
|
||||||
|
const { files } = this.props;
|
||||||
|
return first(Object.keys(files).map(key => files[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPreview() {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
challengeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
challengeType === challengeTypes.html ||
|
||||||
|
challengeType === challengeTypes.modern
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInstructionsPanel({ showToolPanel }) {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
fields: { blockName },
|
||||||
description,
|
description,
|
||||||
instructions,
|
instructions,
|
||||||
videoUrl
|
videoUrl
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
files,
|
|
||||||
output
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<SidePanel
|
||||||
|
className='full-height'
|
||||||
|
description={description}
|
||||||
|
guideUrl={this.getGuideUrl()}
|
||||||
|
instructions={instructions}
|
||||||
|
section={dasherize(blockName)}
|
||||||
|
showToolPanel={showToolPanel}
|
||||||
|
title={this.getBlockNameTitle()}
|
||||||
|
videoUrl={videoUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEditor() {
|
||||||
|
const { files } = this.props;
|
||||||
const challengeFile = first(Object.keys(files).map(key => files[key]));
|
const challengeFile = first(Object.keys(files).map(key => files[key]));
|
||||||
const editors = challengeFile && (
|
return challengeFile && (
|
||||||
|
<Editor {...challengeFile} fileKey={challengeFile.key} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTestOutput() {
|
||||||
|
const { output } = this.props;
|
||||||
|
return (
|
||||||
|
<Output
|
||||||
|
defaultOutput={`
|
||||||
|
/**
|
||||||
|
* Your test output will go here.
|
||||||
|
*/
|
||||||
|
`}
|
||||||
|
output={decodeHTMLEntities(output)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreview() {
|
||||||
|
return (
|
||||||
|
<Preview
|
||||||
|
className='full-height'
|
||||||
|
disableIframe={this.state.resizing}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'>
|
<ReflexContainer key={challengeFile.key} orientation='horizontal'>
|
||||||
<ReflexElement
|
<ReflexElement
|
||||||
flex={1}
|
flex={1}
|
||||||
@ -194,7 +296,7 @@ class ShowClassic extends Component {
|
|||||||
renderOnResizeRate={20}
|
renderOnResizeRate={20}
|
||||||
{...this.resizeProps}
|
{...this.resizeProps}
|
||||||
>
|
>
|
||||||
<Editor {...challengeFile} fileKey={challengeFile.key} />
|
{this.renderEditor()}
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
<ReflexSplitter propagate={true} {...this.resizeProps} />
|
<ReflexSplitter propagate={true} {...this.resizeProps} />
|
||||||
<ReflexElement
|
<ReflexElement
|
||||||
@ -204,53 +306,96 @@ class ShowClassic extends Component {
|
|||||||
renderOnResizeRate={20}
|
renderOnResizeRate={20}
|
||||||
{...this.resizeProps}
|
{...this.resizeProps}
|
||||||
>
|
>
|
||||||
<Output
|
{this.renderTestOutput()}
|
||||||
defaultOutput={`
|
|
||||||
/**
|
|
||||||
* Your test output will go here.
|
|
||||||
*/
|
|
||||||
`}
|
|
||||||
output={decodeHTMLEntities(output)}
|
|
||||||
/>
|
|
||||||
</ReflexElement>
|
</ReflexElement>
|
||||||
</ReflexContainer>
|
</ReflexContainer>
|
||||||
|
)}
|
||||||
|
</ReflexElement>
|
||||||
|
{this.hasPreview() && (
|
||||||
|
<Fragment>
|
||||||
|
<ReflexSplitter propagate={true} {...this.resizeProps} />
|
||||||
|
<ReflexElement flex={0.7} {...this.resizeProps}>
|
||||||
|
{this.renderPreview()}
|
||||||
|
</ReflexElement>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</ReflexContainer>
|
||||||
);
|
);
|
||||||
const showPreview =
|
}
|
||||||
challengeType === challengeTypes.html ||
|
|
||||||
challengeType === challengeTypes.modern;
|
renderMobileLayout() {
|
||||||
const blockNameTitle = `${blockName}: ${title}`;
|
const {
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
videoUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
currentTab,
|
||||||
|
moveToTab
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const editorTabPaneProps = {
|
||||||
|
mountOnEnter: true,
|
||||||
|
unmountOnExit: true
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<Fragment>
|
||||||
<Helmet title={`Learn ${blockNameTitle} | freeCodeCamp.org`} />
|
<Tabs
|
||||||
<ReflexContainer orientation='vertical'>
|
activeKey={currentTab}
|
||||||
<ReflexElement flex={1} {...this.resizeProps}>
|
defaultActiveKey={1}
|
||||||
<SidePanel
|
id='challege-page-tabs'
|
||||||
className='full-height'
|
onSelect={(key) => moveToTab(key)}
|
||||||
description={description}
|
>
|
||||||
guideUrl={createGuideUrl(slug)}
|
<TabPane eventKey={1} title='Instructions'>
|
||||||
instructions={instructions}
|
{ this.renderInstructionsPanel({ showToolPanel: false }) }
|
||||||
section={dasherize(blockName)}
|
</TabPane>
|
||||||
title={blockNameTitle}
|
<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}
|
videoUrl={videoUrl}
|
||||||
/>
|
/>
|
||||||
</ReflexElement>
|
</Fragment>
|
||||||
<ReflexSplitter propagate={true} {...this.resizeProps} />
|
);
|
||||||
<ReflexElement flex={1} {...this.resizeProps}>
|
}
|
||||||
{editors}
|
|
||||||
</ReflexElement>
|
|
||||||
{showPreview && (
|
|
||||||
<ReflexSplitter propagate={true} {...this.resizeProps} />
|
|
||||||
)}
|
|
||||||
{showPreview ? (
|
|
||||||
<ReflexElement flex={0.7} {...this.resizeProps}>
|
|
||||||
<Preview
|
|
||||||
className='full-height'
|
|
||||||
disableIframe={this.state.resizing}
|
|
||||||
/>
|
|
||||||
</ReflexElement>
|
|
||||||
) : null}
|
|
||||||
</ReflexContainer>
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
videoUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LearnLayout>
|
||||||
|
<Helmet
|
||||||
|
title={`Learn ${this.getBlockNameTitle()} | freeCodeCamp.org`}
|
||||||
|
/>
|
||||||
|
<Media query={{ maxWidth: MAX_MOBILE_WIDTH }}>
|
||||||
|
{matches =>
|
||||||
|
matches
|
||||||
|
? this.renderMobileLayout()
|
||||||
|
: this.renderDesktopLayout()
|
||||||
|
}
|
||||||
|
</Media>
|
||||||
<CompletionModal />
|
<CompletionModal />
|
||||||
<HelpModal />
|
<HelpModal />
|
||||||
<VideoModal videoUrl={videoUrl} />
|
<VideoModal videoUrl={videoUrl} />
|
||||||
|
@ -13,6 +13,12 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
.instructions-panel {
|
||||||
|
height: calc(100vh - 112px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.react-monaco-editor-container {
|
.react-monaco-editor-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -24,3 +30,22 @@
|
|||||||
color: #a2bd9b;
|
color: #a2bd9b;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#challege-page-tabs .nav-tabs {
|
||||||
|
margin-left: 2px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#challege-page-tabs .nav-tabs > li {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#challege-page-tabs .nav-tabs > li > a {
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challege-edittor-wrapper {
|
||||||
|
height: calc(100vh - 112px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@ -112,7 +112,7 @@ export class CompletionModal extends Component {
|
|||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
onClick={submitChallenge}
|
onClick={submitChallenge}
|
||||||
>
|
>
|
||||||
Submit and go to next challenge (Ctrl + Enter)
|
Submit and go to next challenge <span className='hidden-xs'>(Ctrl + Enter)</span>
|
||||||
</Button>
|
</Button>
|
||||||
{showDownloadButton ? (
|
{showDownloadButton ? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -7,6 +7,8 @@ import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
|||||||
import ga from '../../../analytics';
|
import ga from '../../../analytics';
|
||||||
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
|
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
|
||||||
|
|
||||||
|
import './help-modal.css';
|
||||||
|
|
||||||
const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) });
|
const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) });
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
@ -31,7 +33,7 @@ export class HelpModal extends Component {
|
|||||||
ga.modalview('/help-modal');
|
ga.modalview('/help-modal');
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Modal onHide={closeHelpModal} show={isOpen}>
|
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>
|
||||||
<Modal.Header
|
<Modal.Header
|
||||||
className='help-modal-header fcc-modal'
|
className='help-modal-header fcc-modal'
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { previewMounted } from '../redux';
|
||||||
|
|
||||||
import './preview.css';
|
import './preview.css';
|
||||||
|
|
||||||
const mainId = 'fcc-main-frame';
|
const mainId = 'fcc-main-frame';
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators(
|
||||||
|
{
|
||||||
|
previewMounted
|
||||||
|
},
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disableIframe: PropTypes.bool
|
disableIframe: PropTypes.bool,
|
||||||
|
previewMounted: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
class Preview extends Component {
|
class Preview extends Component {
|
||||||
@ -19,6 +32,10 @@ class Preview extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.previewMounted();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.disableIframe !== prevProps.disableIframe) {
|
if (this.props.disableIframe !== prevProps.disableIframe) {
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
@ -39,4 +56,7 @@ class Preview extends Component {
|
|||||||
Preview.displayName = 'Preview';
|
Preview.displayName = 'Preview';
|
||||||
Preview.propTypes = propTypes;
|
Preview.propTypes = propTypes;
|
||||||
|
|
||||||
export default Preview;
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Preview);
|
||||||
|
@ -34,6 +34,7 @@ const propTypes = {
|
|||||||
initConsole: PropTypes.func.isRequired,
|
initConsole: PropTypes.func.isRequired,
|
||||||
instructions: PropTypes.string,
|
instructions: PropTypes.string,
|
||||||
section: PropTypes.string,
|
section: PropTypes.string,
|
||||||
|
showToolPanel: PropTypes.bool,
|
||||||
tests: PropTypes.arrayOf(PropTypes.object),
|
tests: PropTypes.arrayOf(PropTypes.object),
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
videoUrl: PropTypes.string
|
videoUrl: PropTypes.string
|
||||||
@ -89,6 +90,7 @@ export class SidePanel extends Component {
|
|||||||
guideUrl,
|
guideUrl,
|
||||||
tests,
|
tests,
|
||||||
section,
|
section,
|
||||||
|
showToolPanel,
|
||||||
videoUrl
|
videoUrl
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
@ -99,7 +101,9 @@ export class SidePanel extends Component {
|
|||||||
<ChallengeTitle>{title}</ChallengeTitle>
|
<ChallengeTitle>{title}</ChallengeTitle>
|
||||||
<ChallengeDescription description={description} instructions={instructions} section={section} />
|
<ChallengeDescription description={description} instructions={instructions} section={section} />
|
||||||
</div>
|
</div>
|
||||||
|
{ showToolPanel && (
|
||||||
<ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />
|
<ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />
|
||||||
|
)}
|
||||||
<TestSuite tests={tests} />
|
<TestSuite tests={tests} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -23,6 +23,7 @@ const mapDispatchToProps = dispatch =>
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
executeChallenge: PropTypes.func.isRequired,
|
executeChallenge: PropTypes.func.isRequired,
|
||||||
guideUrl: PropTypes.string,
|
guideUrl: PropTypes.string,
|
||||||
|
isMobile: PropTypes.bool,
|
||||||
openHelpModal: PropTypes.func.isRequired,
|
openHelpModal: PropTypes.func.isRequired,
|
||||||
openResetModal: PropTypes.func.isRequired,
|
openResetModal: PropTypes.func.isRequired,
|
||||||
openVideoModal: PropTypes.func.isRequired,
|
openVideoModal: PropTypes.func.isRequired,
|
||||||
@ -31,6 +32,7 @@ const propTypes = {
|
|||||||
|
|
||||||
function ToolPanel({
|
function ToolPanel({
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
|
isMobile,
|
||||||
openHelpModal,
|
openHelpModal,
|
||||||
openVideoModal,
|
openVideoModal,
|
||||||
openResetModal,
|
openResetModal,
|
||||||
@ -39,9 +41,11 @@ function ToolPanel({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='tool-panel-group'>
|
<div className={`tool-panel-group ${
|
||||||
|
isMobile ? 'tool-panel-group-mobile' : ''
|
||||||
|
}`}>
|
||||||
<Button block={true} bsStyle='primary' onClick={executeChallenge}>
|
<Button block={true} bsStyle='primary' onClick={executeChallenge}>
|
||||||
Run the Tests
|
{isMobile ? 'Run' : 'Run the Tests'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
@ -49,7 +53,7 @@ function ToolPanel({
|
|||||||
className='btn-primary-invert'
|
className='btn-primary-invert'
|
||||||
onClick={openResetModal}
|
onClick={openResetModal}
|
||||||
>
|
>
|
||||||
Reset All Code
|
{isMobile ? 'Reset' : 'Reset All Code'}
|
||||||
</Button>
|
</Button>
|
||||||
{guideUrl ? (
|
{guideUrl ? (
|
||||||
<Button
|
<Button
|
||||||
@ -59,7 +63,7 @@ function ToolPanel({
|
|||||||
href={guideUrl}
|
href={guideUrl}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
Get a hint
|
{isMobile ? 'Hint' : 'Get a hint'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{videoUrl ? (
|
{videoUrl ? (
|
||||||
@ -69,7 +73,7 @@ function ToolPanel({
|
|||||||
className='btn-primary-invert'
|
className='btn-primary-invert'
|
||||||
onClick={openVideoModal}
|
onClick={openVideoModal}
|
||||||
>
|
>
|
||||||
Watch a video
|
{isMobile ? 'Video' : 'Watch a video'}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
@ -78,7 +82,7 @@ function ToolPanel({
|
|||||||
className='btn-primary-invert'
|
className='btn-primary-invert'
|
||||||
onClick={openHelpModal}
|
onClick={openHelpModal}
|
||||||
>
|
>
|
||||||
Ask for help
|
{isMobile ? 'Help' : 'Ask for help'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -9,3 +9,9 @@
|
|||||||
height: 30vh;
|
height: 30vh;
|
||||||
width: 30vh;
|
width: 30vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
.challenge-success-modal .btn-lg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
.help-modal .btn-lg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
.challenge-preview, .challenge-preview-frame {
|
||||||
|
height: calc(100vh - 112px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.enable-iframe {
|
.enable-iframe {
|
||||||
|
@ -9,3 +9,9 @@
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
.reset-modal .btn-lg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
.tool-panel-group button, .tool-panel-group a {
|
.tool-panel-group button, .tool-panel-group a {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-panel-group-mobile button, .tool-panel-group-mobile a {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-group-mobile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
padding: 0 2px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.tool-panel-group-mobile .btn-block + .btn-block {
|
||||||
|
margin: 0 2px 0 0;
|
||||||
|
}
|
||||||
|
@ -43,7 +43,12 @@ const executeDebounceTimeout = 750;
|
|||||||
|
|
||||||
function updateMainEpic(action$, state$, { document }) {
|
function updateMainEpic(action$, state$, { document }) {
|
||||||
return action$.pipe(
|
return action$.pipe(
|
||||||
ofType(types.updateFile, types.challengeMounted),
|
ofType(
|
||||||
|
types.updateFile,
|
||||||
|
types.previewMounted,
|
||||||
|
types.challengeMounted,
|
||||||
|
types.resetChallenge
|
||||||
|
),
|
||||||
filter(() => {
|
filter(() => {
|
||||||
const { challengeType } = challengeMetaSelector(state$.value);
|
const { challengeType } = challengeMetaSelector(state$.value);
|
||||||
return (
|
return (
|
||||||
|
@ -67,6 +67,7 @@ export const types = createTypes(
|
|||||||
'closeModal',
|
'closeModal',
|
||||||
'openModal',
|
'openModal',
|
||||||
|
|
||||||
|
'previewMounted',
|
||||||
'challengeMounted',
|
'challengeMounted',
|
||||||
'checkChallenge',
|
'checkChallenge',
|
||||||
'executeChallenge',
|
'executeChallenge',
|
||||||
@ -74,6 +75,8 @@ export const types = createTypes(
|
|||||||
'submitChallenge',
|
'submitChallenge',
|
||||||
'submitComplete',
|
'submitComplete',
|
||||||
|
|
||||||
|
'moveToTab',
|
||||||
|
|
||||||
...createAsyncTypes('fetchIdToNameMap')
|
...createAsyncTypes('fetchIdToNameMap')
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
@ -143,6 +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 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);
|
||||||
@ -150,6 +154,9 @@ export const resetChallenge = createAction(types.resetChallenge);
|
|||||||
export const submitChallenge = createAction(types.submitChallenge);
|
export const submitChallenge = createAction(types.submitChallenge);
|
||||||
export const submitComplete = createAction(types.submitComplete);
|
export const submitComplete = createAction(types.submitComplete);
|
||||||
|
|
||||||
|
export const moveToTab = createAction(types.moveToTab);
|
||||||
|
|
||||||
|
export const currentTabSelector = state => state[ns].currentTab;
|
||||||
export const challengeFilesSelector = state => state[ns].challengeFiles;
|
export const challengeFilesSelector = state => state[ns].challengeFiles;
|
||||||
export const challengeIdToNameMapSelector = state =>
|
export const challengeIdToNameMapSelector = state =>
|
||||||
state[ns].challengeIdToNameMap;
|
state[ns].challengeIdToNameMap;
|
||||||
@ -234,6 +241,7 @@ export const reducer = handleActions(
|
|||||||
|
|
||||||
[types.resetChallenge]: state => ({
|
[types.resetChallenge]: state => ({
|
||||||
...state,
|
...state,
|
||||||
|
currentTab: 2,
|
||||||
challengeFiles: {
|
challengeFiles: {
|
||||||
...Object.keys(state.challengeFiles)
|
...Object.keys(state.challengeFiles)
|
||||||
.map(key => state.challengeFiles[key])
|
.map(key => state.challengeFiles[key])
|
||||||
@ -291,6 +299,14 @@ export const reducer = handleActions(
|
|||||||
...state.modal,
|
...state.modal,
|
||||||
[payload]: true
|
[payload]: true
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
[types.moveToTab]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
currentTab: payload
|
||||||
|
}),
|
||||||
|
[types.executeChallenge]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
currentTab: 3
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
|
@ -44,6 +44,7 @@ function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{block} | freeCodeCamp</title>
|
<title>{block} | freeCodeCamp</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
<div className='intro-layout-container'>
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<div
|
<div
|
||||||
className='intro-layout'
|
className='intro-layout'
|
||||||
@ -69,6 +70,7 @@ function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
|
|||||||
{allChallengeNode ? renderMenuItems(allChallengeNode) : null}
|
{allChallengeNode ? renderMenuItems(allChallengeNode) : null}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
</LearnLayout>
|
</LearnLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
@media screen, (max-width: 767px) {
|
||||||
|
.intro-layout-container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.intro-layout {
|
.intro-layout {
|
||||||
margin-top: 1.45rem;
|
margin-top: 1.45rem;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user