From d4b07f47aba8295950d28c1d95f519752510836b Mon Sep 17 00:00:00 2001 From: Dmytro Yarmak Date: Fri, 7 Dec 2018 14:08:32 +0200 Subject: [PATCH] fix(learn): implemented curriculum layout for mobile devices (#17467) remove useless changes --- client/src/components/layouts/layout.css | 2 +- client/src/pages/learn.css | 7 + .../src/templates/Challenges/classic/Show.js | 281 +++++++++++++----- .../templates/Challenges/classic/classic.css | 27 +- .../Challenges/components/CompletionModal.js | 2 +- .../Challenges/components/HelpModal.js | 4 +- .../Challenges/components/Preview.js | 24 +- .../Challenges/components/Side-Panel.js | 6 +- .../Challenges/components/Tool-Panel.js | 16 +- .../components/completion-modal.css | 6 + .../Challenges/components/help-modal.css | 5 + .../Challenges/components/preview.css | 7 + .../Challenges/components/reset-modal.css | 8 +- .../Challenges/components/tool-panel.css | 14 + .../redux/execute-challenge-epic.js | 7 +- .../src/templates/Challenges/redux/index.js | 16 + client/src/templates/Introduction/Intro.js | 52 ++-- client/src/templates/Introduction/intro.css | 6 + 18 files changed, 382 insertions(+), 108 deletions(-) create mode 100644 client/src/templates/Challenges/components/help-modal.css diff --git a/client/src/components/layouts/layout.css b/client/src/components/layouts/layout.css index 398be36a0b..2868e1bee0 100644 --- a/client/src/components/layouts/layout.css +++ b/client/src/components/layouts/layout.css @@ -624,4 +624,4 @@ pre tt:after { } .default-layout { margin-top: 38px; -} \ No newline at end of file +} diff --git a/client/src/pages/learn.css b/client/src/pages/learn.css index 6fd8f19082..b057415234 100644 --- a/client/src/pages/learn.css +++ b/client/src/pages/learn.css @@ -25,6 +25,13 @@ 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 { width: 100%; margin: 0 18px; diff --git a/client/src/templates/Challenges/classic/Show.js b/client/src/templates/Challenges/classic/Show.js index 6bd2012ec1..3aa39fa723 100644 --- a/client/src/templates/Challenges/classic/Show.js +++ b/client/src/templates/Challenges/classic/Show.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { createSelector } from 'reselect'; @@ -7,6 +7,8 @@ import Helmet from 'react-helmet'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { graphql } from 'gatsby'; import { first } from 'lodash'; +import { Tabs, TabPane } from '@freecodecamp/react-bootstrap'; +import Media from 'react-media'; import LearnLayout from '../../../components/layouts/Learn'; import Editor from './Editor'; @@ -17,6 +19,7 @@ import CompletionModal from '../components/CompletionModal'; import HelpModal from '../components/HelpModal'; import VideoModal from '../components/VideoModal'; import ResetModal from '../components/ResetModal'; +import ToolPanel from '../components/Tool-Panel'; import { randomCompliment } from '../utils/get-words'; import { createGuideUrl } from '../utils'; @@ -27,11 +30,13 @@ import { createFiles, challengeFilesSelector, challengeTestsSelector, + currentTabSelector, initTests, updateChallengeMeta, challengeMounted, updateSuccessMessage, - consoleOutputSelector + consoleOutputSelector, + moveToTab } from '../redux'; import './classic.css'; @@ -43,16 +48,19 @@ const mapStateToProps = createSelector( challengeFilesSelector, challengeTestsSelector, consoleOutputSelector, - (files, tests, output) => ({ + currentTabSelector, + (files, tests, output, currentTab) => ({ files, tests, - output + output, + currentTab }) ); const mapDispatchToProps = dispatch => bindActionCreators( { + moveToTab, createFiles, initTests, updateChallengeMeta, @@ -65,6 +73,7 @@ const mapDispatchToProps = dispatch => const propTypes = { challengeMounted: PropTypes.func.isRequired, createFiles: PropTypes.func.isRequired, + currentTab: PropTypes.number, data: PropTypes.shape({ challengeNode: ChallengeNode }), @@ -72,6 +81,7 @@ const propTypes = { key: PropTypes.string }), initTests: PropTypes.func.isRequired, + moveToTab: PropTypes.func.isRequired, output: PropTypes.string, pageContext: PropTypes.shape({ challengeMeta: PropTypes.shape({ @@ -88,6 +98,8 @@ const propTypes = { updateSuccessMessage: PropTypes.func.isRequired }; +const MAX_MOBILE_WIDTH = 767; + class ShowClassic extends Component { constructor() { super(); @@ -101,7 +113,6 @@ class ShowClassic extends Component { resizing: false }; } - onResize() { this.setState({ resizing: true }); } @@ -169,88 +180,222 @@ class ShowClassic extends Component { } } - render() { + getBlockNameTitle() { const { data: { challengeNode: { - challengeType, - fields: { blockName, slug }, - title, + fields: { blockName }, + 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, instructions, videoUrl } - }, - files, - output + } } = this.props; + return ( + + ); + } + + renderEditor() { + const { files } = this.props; const challengeFile = first(Object.keys(files).map(key => files[key])); - const editors = challengeFile && ( - - - - - - - + ); + } + + renderTestOutput() { + const { output } = this.props; + return ( + + output={decodeHTMLEntities(output)} + /> + ); + } + + renderPreview() { + return ( + + ); + } + + renderDesktopLayout() { + const challengeFile = this.getChallengeFile(); + return ( + + + {this.renderInstructionsPanel({ showToolPanel: true })} + + + {challengeFile && ( + + + {this.renderEditor()} + + + + {this.renderTestOutput()} + + + )} + + {this.hasPreview() && ( + + + + {this.renderPreview()} + + + )} ); - const showPreview = - challengeType === challengeTypes.html || - challengeType === challengeTypes.modern; - const blockNameTitle = `${blockName}: ${title}`; + } + + renderMobileLayout() { + const { + data: { + challengeNode: { + videoUrl + } + }, + currentTab, + moveToTab + } = this.props; + + const editorTabPaneProps = { + mountOnEnter: true, + unmountOnExit: true + }; + + return ( + + moveToTab(key)} + > + + { this.renderInstructionsPanel({ showToolPanel: false }) } + + +
+ {this.renderEditor()} +
+
+ +
+ {this.renderTestOutput()} +
+
+ {this.hasPreview() && ( + + {this.renderPreview()} + + )} +
+ +
+ ); + } + + render() { + const { + data: { + challengeNode: { + videoUrl + } + } + } = this.props; + return ( - - - - - - - - {editors} - - {showPreview && ( - - )} - {showPreview ? ( - - - - ) : null} - - + + + {matches => + matches + ? this.renderMobileLayout() + : this.renderDesktopLayout() + } + diff --git a/client/src/templates/Challenges/classic/classic.css b/client/src/templates/Challenges/classic/classic.css index ef40a1a489..6d38590827 100644 --- a/client/src/templates/Challenges/classic/classic.css +++ b/client/src/templates/Challenges/classic/classic.css @@ -13,6 +13,12 @@ overflow-y: auto; } +@media screen, (max-width: 767px) { + .instructions-panel { + height: calc(100vh - 112px); + } +} + .react-monaco-editor-container { width: 100%; height: 100%; @@ -23,4 +29,23 @@ .monaco-menu .action-label { color: #a2bd9b; letter-spacing: 0.02em; -} \ No newline at end of file +} + +#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; +} diff --git a/client/src/templates/Challenges/components/CompletionModal.js b/client/src/templates/Challenges/components/CompletionModal.js index 216c09a3a1..b7c7908b7d 100644 --- a/client/src/templates/Challenges/components/CompletionModal.js +++ b/client/src/templates/Challenges/components/CompletionModal.js @@ -112,7 +112,7 @@ export class CompletionModal extends Component { bsStyle='primary' onClick={submitChallenge} > - Submit and go to next challenge (Ctrl + Enter) + Submit and go to next challenge (Ctrl + Enter) {showDownloadButton ? ( {guideUrl ? ( ) : null} {videoUrl ? ( @@ -69,7 +73,7 @@ function ToolPanel({ className='btn-primary-invert' onClick={openVideoModal} > - Watch a video + {isMobile ? 'Video' : 'Watch a video'} ) : null} diff --git a/client/src/templates/Challenges/components/completion-modal.css b/client/src/templates/Challenges/components/completion-modal.css index 1e8c67eedd..0970bdc871 100644 --- a/client/src/templates/Challenges/components/completion-modal.css +++ b/client/src/templates/Challenges/components/completion-modal.css @@ -9,3 +9,9 @@ height: 30vh; width: 30vh; } + +@media screen, (max-width: 767px) { + .challenge-success-modal .btn-lg { + font-size: 16px; + } +} diff --git a/client/src/templates/Challenges/components/help-modal.css b/client/src/templates/Challenges/components/help-modal.css new file mode 100644 index 0000000000..ac54f7f115 --- /dev/null +++ b/client/src/templates/Challenges/components/help-modal.css @@ -0,0 +1,5 @@ +@media screen, (max-width: 767px) { + .help-modal .btn-lg { + font-size: 16px; + } +} diff --git a/client/src/templates/Challenges/components/preview.css b/client/src/templates/Challenges/components/preview.css index 338dc0f19e..24498dc399 100644 --- a/client/src/templates/Challenges/components/preview.css +++ b/client/src/templates/Challenges/components/preview.css @@ -3,6 +3,13 @@ width: 100%; padding: 0; margin: 0; + border: none; +} + +@media screen, (max-width: 767px) { + .challenge-preview, .challenge-preview-frame { + height: calc(100vh - 112px); + } } .enable-iframe { diff --git a/client/src/templates/Challenges/components/reset-modal.css b/client/src/templates/Challenges/components/reset-modal.css index 5e561d1fe7..76a560e495 100644 --- a/client/src/templates/Challenges/components/reset-modal.css +++ b/client/src/templates/Challenges/components/reset-modal.css @@ -8,4 +8,10 @@ color: #fff; font-size: 28px; text-shadow: none; -} \ No newline at end of file +} + +@media screen, (max-width: 767px) { + .reset-modal .btn-lg { + font-size: 16px; + } +} diff --git a/client/src/templates/Challenges/components/tool-panel.css b/client/src/templates/Challenges/components/tool-panel.css index fc70fdc2b3..fe5e09fce9 100644 --- a/client/src/templates/Challenges/components/tool-panel.css +++ b/client/src/templates/Challenges/components/tool-panel.css @@ -1,3 +1,17 @@ .tool-panel-group button, .tool-panel-group a { 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; +} diff --git a/client/src/templates/Challenges/redux/execute-challenge-epic.js b/client/src/templates/Challenges/redux/execute-challenge-epic.js index 57f657da18..dd45bf8c2d 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-epic.js +++ b/client/src/templates/Challenges/redux/execute-challenge-epic.js @@ -43,7 +43,12 @@ const executeDebounceTimeout = 750; function updateMainEpic(action$, state$, { document }) { return action$.pipe( - ofType(types.updateFile, types.challengeMounted), + ofType( + types.updateFile, + types.previewMounted, + types.challengeMounted, + types.resetChallenge + ), filter(() => { const { challengeType } = challengeMetaSelector(state$.value); return ( diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 4dd3f5eece..46abdd15c1 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -67,6 +67,7 @@ export const types = createTypes( 'closeModal', 'openModal', + 'previewMounted', 'challengeMounted', 'checkChallenge', 'executeChallenge', @@ -74,6 +75,8 @@ export const types = createTypes( 'submitChallenge', 'submitComplete', + 'moveToTab', + ...createAsyncTypes('fetchIdToNameMap') ], ns @@ -143,6 +146,7 @@ export const noStoredCodeFound = createAction(types.noStoredCodeFound); export const closeModal = createAction(types.closeModal); export const openModal = createAction(types.openModal); +export const previewMounted = createAction(types.challengeMounted); export const challengeMounted = createAction(types.challengeMounted); export const checkChallenge = createAction(types.checkChallenge); export const executeChallenge = createAction(types.executeChallenge); @@ -150,6 +154,9 @@ export const resetChallenge = createAction(types.resetChallenge); export const submitChallenge = createAction(types.submitChallenge); 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 challengeIdToNameMapSelector = state => state[ns].challengeIdToNameMap; @@ -234,6 +241,7 @@ export const reducer = handleActions( [types.resetChallenge]: state => ({ ...state, + currentTab: 2, challengeFiles: { ...Object.keys(state.challengeFiles) .map(key => state.challengeFiles[key]) @@ -291,6 +299,14 @@ export const reducer = handleActions( ...state.modal, [payload]: true } + }), + [types.moveToTab]: (state, { payload }) => ({ + ...state, + currentTab: payload + }), + [types.executeChallenge]: (state, { payload }) => ({ + ...state, + currentTab: 3 }) }, initialState diff --git a/client/src/templates/Introduction/Intro.js b/client/src/templates/Introduction/Intro.js index ed93aef7af..e2b9f1cbeb 100644 --- a/client/src/templates/Introduction/Intro.js +++ b/client/src/templates/Introduction/Intro.js @@ -44,31 +44,33 @@ function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) { {block} | freeCodeCamp - -
- - - - Go to the first lesson - - - - - - -
-
- -

Upcoming Lessons

- - {allChallengeNode ? renderMenuItems(allChallengeNode) : null} - -
+
+ +
+ + + + Go to the first lesson + + + + + + +
+
+ +

Upcoming Lessons

+ + {allChallengeNode ? renderMenuItems(allChallengeNode) : null} + +
+
); } diff --git a/client/src/templates/Introduction/intro.css b/client/src/templates/Introduction/intro.css index 5fb9d1946d..1e82af8973 100644 --- a/client/src/templates/Introduction/intro.css +++ b/client/src/templates/Introduction/intro.css @@ -1,3 +1,9 @@ +@media screen, (max-width: 767px) { + .intro-layout-container { + padding: 0 10px; + } +} + .intro-layout { margin-top: 1.45rem; }