feat(client): migrate to ts - (HelpModal, Preview, VideoModal, Side-Panel) (#42857)
* refactor: migrate HelpModal, Preview, VideoModal, Side-Panel * refactor: import order Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
16
client/src/declarations.d.ts
vendored
16
client/src/declarations.d.ts
vendored
@ -9,3 +9,19 @@ declare module '*.svg' {
|
|||||||
const content: string;
|
const content: string;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface Global {
|
||||||
|
MathJax: {
|
||||||
|
Hub: {
|
||||||
|
Config: (attributes: {
|
||||||
|
tex2jax: {
|
||||||
|
inlineMath: Array<string[]>;
|
||||||
|
processEscapes: boolean;
|
||||||
|
processClass: string;
|
||||||
|
};
|
||||||
|
}) => void;
|
||||||
|
Queue: (attributes: unknown[]) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// Package Utilities
|
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
@ -8,49 +7,47 @@ import { HandlerProps } from 'react-reflex';
|
|||||||
import Media from 'react-responsive';
|
import Media from 'react-responsive';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { createStructuredSelector } from 'reselect';
|
import { createStructuredSelector } from 'reselect';
|
||||||
|
|
||||||
// Local Utilities
|
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import { challengeTypes } from '../../../../utils/challenge-types';
|
import { challengeTypes } from '../../../../utils/challenge-types';
|
||||||
|
|
||||||
import LearnLayout from '../../../components/layouts/learn';
|
import LearnLayout from '../../../components/layouts/learn';
|
||||||
import {
|
import {
|
||||||
ChallengeNodeType,
|
|
||||||
ChallengeFiles,
|
|
||||||
ChallengeFile,
|
ChallengeFile,
|
||||||
|
ChallengeFiles,
|
||||||
ChallengeMetaType,
|
ChallengeMetaType,
|
||||||
Test,
|
ChallengeNodeType,
|
||||||
ResizePropsType
|
ResizePropsType,
|
||||||
|
Test
|
||||||
} from '../../../redux/prop-types';
|
} from '../../../redux/prop-types';
|
||||||
import { isContained } from '../../../utils/is-contained';
|
import { isContained } from '../../../utils/is-contained';
|
||||||
import ChallengeDescription from '../components/Challenge-Description';
|
import ChallengeDescription from '../components/Challenge-Description';
|
||||||
import HelpModal from '../components/HelpModal';
|
|
||||||
import Hotkeys from '../components/Hotkeys';
|
import Hotkeys from '../components/Hotkeys';
|
||||||
import Preview from '../components/Preview';
|
|
||||||
import ResetModal from '../components/ResetModal';
|
import ResetModal from '../components/ResetModal';
|
||||||
import SidePanel from '../components/Side-Panel';
|
|
||||||
import VideoModal from '../components/VideoModal';
|
|
||||||
import ChallengeTitle from '../components/challenge-title';
|
import ChallengeTitle from '../components/challenge-title';
|
||||||
import CompletionModal from '../components/completion-modal';
|
import CompletionModal from '../components/completion-modal';
|
||||||
|
import HelpModal from '../components/help-modal';
|
||||||
import Output from '../components/output';
|
import Output from '../components/output';
|
||||||
|
import Preview from '../components/preview';
|
||||||
|
import SidePanel from '../components/side-panel';
|
||||||
|
import VideoModal from '../components/video-modal';
|
||||||
import {
|
import {
|
||||||
createFiles,
|
cancelTests,
|
||||||
challengeFilesSelector,
|
challengeFilesSelector,
|
||||||
|
challengeMounted,
|
||||||
challengeTestsSelector,
|
challengeTestsSelector,
|
||||||
|
consoleOutputSelector,
|
||||||
|
createFiles,
|
||||||
|
executeChallenge,
|
||||||
initConsole,
|
initConsole,
|
||||||
initTests,
|
initTests,
|
||||||
updateChallengeMeta,
|
isChallengeCompletedSelector,
|
||||||
challengeMounted,
|
updateChallengeMeta
|
||||||
consoleOutputSelector,
|
|
||||||
executeChallenge,
|
|
||||||
cancelTests,
|
|
||||||
isChallengeCompletedSelector
|
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import { getGuideUrl } from '../utils';
|
import { getGuideUrl } from '../utils';
|
||||||
import DesktopLayout from './DesktopLayout';
|
import DesktopLayout from './DesktopLayout';
|
||||||
import MobileLayout from './MobileLayout';
|
import MobileLayout from './MobileLayout';
|
||||||
import MultifileEditor from './MultifileEditor';
|
import MultifileEditor from './MultifileEditor';
|
||||||
|
|
||||||
// Styles
|
|
||||||
import './classic.css';
|
import './classic.css';
|
||||||
import '../components/test-frame.css';
|
import '../components/test-frame.css';
|
||||||
|
|
||||||
@ -124,7 +121,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
|||||||
static displayName: string;
|
static displayName: string;
|
||||||
containerRef: React.RefObject<unknown>;
|
containerRef: React.RefObject<unknown>;
|
||||||
editorRef: React.RefObject<unknown>;
|
editorRef: React.RefObject<unknown>;
|
||||||
instructionsPanelRef: React.RefObject<HTMLElement>;
|
instructionsPanelRef: React.RefObject<HTMLDivElement>;
|
||||||
resizeProps: ResizePropsType;
|
resizeProps: ResizePropsType;
|
||||||
|
|
||||||
constructor(props: ShowClassicProps) {
|
constructor(props: ShowClassicProps) {
|
||||||
@ -309,7 +306,6 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
|||||||
{title}
|
{title}
|
||||||
</ChallengeTitle>
|
</ChallengeTitle>
|
||||||
}
|
}
|
||||||
className='full-height'
|
|
||||||
guideUrl={getGuideUrl({ forumTopicId, title })}
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||||
instructionsPanelRef={this.instructionsPanelRef}
|
instructionsPanelRef={this.instructionsPanelRef}
|
||||||
showToolPanel={showToolPanel}
|
showToolPanel={showToolPanel}
|
@ -1,90 +0,0 @@
|
|||||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
|
|
||||||
import envData from '../../../../../config/env.json';
|
|
||||||
import { executeGA } from '../../../redux';
|
|
||||||
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
|
|
||||||
|
|
||||||
import './help-modal.css';
|
|
||||||
|
|
||||||
const { forumLocation } = envData;
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({ isOpen: isHelpModalOpenSelector(state) });
|
|
||||||
const mapDispatchToProps = dispatch =>
|
|
||||||
bindActionCreators(
|
|
||||||
{ createQuestion, executeGA, closeHelpModal: () => closeModal('help') },
|
|
||||||
dispatch
|
|
||||||
);
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
closeHelpModal: PropTypes.func.isRequired,
|
|
||||||
createQuestion: PropTypes.func.isRequired,
|
|
||||||
executeGA: PropTypes.func,
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
t: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const RSA = forumLocation + '/t/19514';
|
|
||||||
|
|
||||||
export class HelpModal extends Component {
|
|
||||||
render() {
|
|
||||||
const { isOpen, closeHelpModal, createQuestion, executeGA, t } = this.props;
|
|
||||||
if (isOpen) {
|
|
||||||
executeGA({ type: 'modal', data: '/help-modal' });
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>
|
|
||||||
<Modal.Header
|
|
||||||
className='help-modal-header fcc-modal'
|
|
||||||
closeButton={true}
|
|
||||||
>
|
|
||||||
<Modal.Title className='text-center'>
|
|
||||||
{t('buttons.ask-for-help')}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body className='help-modal-body text-center'>
|
|
||||||
<h3>
|
|
||||||
<Trans i18nKey='learn.tried-rsa'>
|
|
||||||
<a
|
|
||||||
href={RSA}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
title={t('learn.rsa')}
|
|
||||||
>
|
|
||||||
placeholder
|
|
||||||
</a>
|
|
||||||
</Trans>
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
onClick={createQuestion}
|
|
||||||
>
|
|
||||||
{t('buttons.create-post')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
onClick={closeHelpModal}
|
|
||||||
>
|
|
||||||
{t('buttons.cancel')}
|
|
||||||
</Button>
|
|
||||||
</Modal.Body>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HelpModal.displayName = 'HelpModal';
|
|
||||||
HelpModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(withTranslation()(HelpModal));
|
|
@ -1,66 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
|
|
||||||
import { previewMounted } from '../redux';
|
|
||||||
|
|
||||||
import './preview.css';
|
|
||||||
|
|
||||||
const mainId = 'fcc-main-frame';
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
|
||||||
bindActionCreators(
|
|
||||||
{
|
|
||||||
previewMounted
|
|
||||||
},
|
|
||||||
dispatch
|
|
||||||
);
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
disableIframe: PropTypes.bool,
|
|
||||||
previewMounted: PropTypes.func.isRequired,
|
|
||||||
t: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
class Preview extends Component {
|
|
||||||
constructor(...props) {
|
|
||||||
super(...props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
iframeStatus: props.disableIframe
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.previewMounted();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.disableIframe !== prevProps.disableIframe) {
|
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
|
||||||
this.setState({ iframeStatus: !this.state.iframeStatus });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { t } = this.props;
|
|
||||||
const iframeToggle = this.state.iframeStatus ? 'disable' : 'enable';
|
|
||||||
return (
|
|
||||||
<div className={`notranslate challenge-preview ${iframeToggle}-iframe`}>
|
|
||||||
<iframe
|
|
||||||
className={'challenge-preview-frame'}
|
|
||||||
id={mainId}
|
|
||||||
title={t('learn.chal-preview')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Preview.displayName = 'Preview';
|
|
||||||
Preview.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(withTranslation()(Preview));
|
|
@ -1,81 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { mathJaxScriptLoader } from '../../../utils/script-loaders';
|
|
||||||
import { challengeTestsSelector } from '../redux';
|
|
||||||
import TestSuite from './Test-Suite';
|
|
||||||
import ToolPanel from './Tool-Panel';
|
|
||||||
|
|
||||||
import './side-panel.css';
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(challengeTestsSelector, tests => ({
|
|
||||||
tests
|
|
||||||
}));
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
block: PropTypes.string,
|
|
||||||
challengeDescription: PropTypes.element.isRequired,
|
|
||||||
challengeTitle: PropTypes.element.isRequired,
|
|
||||||
guideUrl: PropTypes.string,
|
|
||||||
instructionsPanelRef: PropTypes.any.isRequired,
|
|
||||||
showToolPanel: PropTypes.bool,
|
|
||||||
tests: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
videoUrl: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SidePanel extends Component {
|
|
||||||
componentDidMount() {
|
|
||||||
const MathJax = global.MathJax;
|
|
||||||
const mathJaxMountPoint = document.querySelector('#mathjax');
|
|
||||||
const mathJaxChallenge =
|
|
||||||
this.props.block === 'rosetta-code' ||
|
|
||||||
this.props.block === 'project-euler';
|
|
||||||
if (MathJax) {
|
|
||||||
// Configure MathJax when it's loaded and
|
|
||||||
// users navigate from another challenge
|
|
||||||
MathJax.Hub.Config({
|
|
||||||
tex2jax: {
|
|
||||||
inlineMath: [
|
|
||||||
['$', '$'],
|
|
||||||
['\\(', '\\)']
|
|
||||||
],
|
|
||||||
processEscapes: true,
|
|
||||||
processClass: 'rosetta-code|project-euler'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
MathJax.Hub.Queue([
|
|
||||||
'Typeset',
|
|
||||||
MathJax.Hub,
|
|
||||||
document.querySelector('.rosetta-code'),
|
|
||||||
document.querySelector('.project-euler')
|
|
||||||
]);
|
|
||||||
} else if (!mathJaxMountPoint && mathJaxChallenge) {
|
|
||||||
mathJaxScriptLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { instructionsPanelRef, guideUrl, tests, showToolPanel, videoUrl } =
|
|
||||||
this.props;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='instructions-panel'
|
|
||||||
ref={instructionsPanelRef}
|
|
||||||
role='complementary'
|
|
||||||
tabIndex='-1'
|
|
||||||
>
|
|
||||||
{this.props.challengeTitle}
|
|
||||||
{this.props.challengeDescription}
|
|
||||||
{showToolPanel && <ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />}
|
|
||||||
<TestSuite tests={tests} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SidePanel.displayName = 'SidePanel';
|
|
||||||
SidePanel.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(SidePanel);
|
|
@ -1,67 +0,0 @@
|
|||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
|
|
||||||
import { executeGA } from '../../../redux';
|
|
||||||
import { closeModal, isVideoModalOpenSelector } from '../redux';
|
|
||||||
|
|
||||||
import './video-modal.css';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({ isOpen: isVideoModalOpenSelector(state) });
|
|
||||||
const mapDispatchToProps = dispatch =>
|
|
||||||
bindActionCreators(
|
|
||||||
{ closeVideoModal: () => closeModal('video'), executeGA },
|
|
||||||
dispatch
|
|
||||||
);
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
closeVideoModal: PropTypes.func.isRequired,
|
|
||||||
executeGA: PropTypes.func,
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
videoUrl: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export class VideoModal extends Component {
|
|
||||||
render() {
|
|
||||||
const { isOpen, closeVideoModal, videoUrl, executeGA, t } = this.props;
|
|
||||||
if (isOpen) {
|
|
||||||
executeGA({ type: 'modal', data: '/completion-modal' });
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
dialogClassName='video-modal'
|
|
||||||
onHide={closeVideoModal}
|
|
||||||
show={isOpen}
|
|
||||||
>
|
|
||||||
<Modal.Header
|
|
||||||
className='video-modal-header fcc-modal'
|
|
||||||
closeButton={true}
|
|
||||||
>
|
|
||||||
<Modal.Title className='text-center'>
|
|
||||||
{t('buttons.watch-video')}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body className='video-modal-body'>
|
|
||||||
<iframe
|
|
||||||
frameBorder='0'
|
|
||||||
src={videoUrl}
|
|
||||||
title={t('buttons.watch-video')}
|
|
||||||
/>
|
|
||||||
<p>{t('learn.scrimba-tip')}</p>
|
|
||||||
</Modal.Body>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoModal.displayName = 'VideoModal';
|
|
||||||
VideoModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(withTranslation()(VideoModal));
|
|
90
client/src/templates/Challenges/components/help-modal.tsx
Normal file
90
client/src/templates/Challenges/components/help-modal.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||||
|
import React from 'react';
|
||||||
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import envData from '../../../../../config/env.json';
|
||||||
|
import { executeGA } from '../../../redux';
|
||||||
|
import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux';
|
||||||
|
|
||||||
|
import './help-modal.css';
|
||||||
|
|
||||||
|
interface HelpModalProps {
|
||||||
|
closeHelpModal: () => void;
|
||||||
|
createQuestion: () => void;
|
||||||
|
executeGA: (attributes: { type: string; data: string }) => void;
|
||||||
|
isOpen?: boolean;
|
||||||
|
t: (text: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { forumLocation } = envData;
|
||||||
|
|
||||||
|
const mapStateToProps = (state: unknown) => ({
|
||||||
|
isOpen: isHelpModalOpenSelector(state) as boolean
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
|
bindActionCreators(
|
||||||
|
{ createQuestion, executeGA, closeHelpModal: () => closeModal('help') },
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
|
const RSA = forumLocation + '/t/19514';
|
||||||
|
|
||||||
|
export function HelpModal({
|
||||||
|
closeHelpModal,
|
||||||
|
createQuestion,
|
||||||
|
executeGA,
|
||||||
|
isOpen,
|
||||||
|
t
|
||||||
|
}: HelpModalProps): JSX.Element {
|
||||||
|
if (isOpen) {
|
||||||
|
executeGA({ type: 'modal', data: '/help-modal' });
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal dialogClassName='help-modal' onHide={closeHelpModal} show={isOpen}>
|
||||||
|
<Modal.Header className='help-modal-header fcc-modal' closeButton={true}>
|
||||||
|
<Modal.Title className='text-center'>
|
||||||
|
{t('buttons.ask-for-help')}
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className='help-modal-body text-center'>
|
||||||
|
<h3>
|
||||||
|
<Trans i18nKey='learn.tried-rsa'>
|
||||||
|
<a
|
||||||
|
href={RSA}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
title={t('learn.rsa')}
|
||||||
|
>
|
||||||
|
placeholder
|
||||||
|
</a>
|
||||||
|
</Trans>
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={createQuestion}
|
||||||
|
>
|
||||||
|
{t('buttons.create-post')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
onClick={closeHelpModal}
|
||||||
|
>
|
||||||
|
{t('buttons.cancel')}
|
||||||
|
</Button>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HelpModal.displayName = 'HelpModal';
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(withTranslation()(HelpModal));
|
52
client/src/templates/Challenges/components/preview.tsx
Normal file
52
client/src/templates/Challenges/components/preview.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { previewMounted } from '../redux';
|
||||||
|
|
||||||
|
import './preview.css';
|
||||||
|
|
||||||
|
const mainId = 'fcc-main-frame';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
|
bindActionCreators(
|
||||||
|
{
|
||||||
|
previewMounted
|
||||||
|
},
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
|
interface PreviewProps {
|
||||||
|
className?: string;
|
||||||
|
disableIframe?: boolean;
|
||||||
|
previewMounted: () => void;
|
||||||
|
t: (text: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
|
||||||
|
const [iframeStatus, setIframeStatus] = useState<boolean | undefined>(false);
|
||||||
|
const iframeToggle = iframeStatus ? 'disable' : 'enable';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
previewMounted();
|
||||||
|
}, [previewMounted]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIframeStatus(disableIframe);
|
||||||
|
}, [disableIframe]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`notranslate challenge-preview ${iframeToggle}-iframe`}>
|
||||||
|
<iframe
|
||||||
|
className={'challenge-preview-frame'}
|
||||||
|
id={mainId}
|
||||||
|
title={t('learn.chal-preview')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Preview.displayName = 'Preview';
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(withTranslation()(Preview));
|
86
client/src/templates/Challenges/components/side-panel.tsx
Normal file
86
client/src/templates/Challenges/components/side-panel.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useEffect, ReactElement } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { mathJaxScriptLoader } from '../../../utils/script-loaders';
|
||||||
|
import { challengeTestsSelector } from '../redux';
|
||||||
|
import TestSuite from './Test-Suite';
|
||||||
|
import ToolPanel from './Tool-Panel';
|
||||||
|
|
||||||
|
import './side-panel.css';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
challengeTestsSelector,
|
||||||
|
(tests: Record<string, unknown>[]) => ({
|
||||||
|
tests
|
||||||
|
})
|
||||||
|
);
|
||||||
|
interface SidePanelProps {
|
||||||
|
block: string;
|
||||||
|
challengeDescription: ReactElement;
|
||||||
|
challengeTitle: ReactElement;
|
||||||
|
guideUrl?: string;
|
||||||
|
instructionsPanelRef: React.RefObject<HTMLDivElement>;
|
||||||
|
showToolPanel: boolean;
|
||||||
|
tests?: Record<string, unknown>[];
|
||||||
|
videoUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidePanel({
|
||||||
|
block,
|
||||||
|
challengeDescription,
|
||||||
|
challengeTitle,
|
||||||
|
guideUrl,
|
||||||
|
instructionsPanelRef,
|
||||||
|
showToolPanel = false,
|
||||||
|
tests,
|
||||||
|
videoUrl
|
||||||
|
}: SidePanelProps): JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
const MathJax = global.MathJax;
|
||||||
|
const mathJaxMountPoint = document.querySelector('#mathjax');
|
||||||
|
const mathJaxChallenge =
|
||||||
|
block === 'rosetta-code' || block === 'project-euler';
|
||||||
|
if (MathJax) {
|
||||||
|
// Configure MathJax when it's loaded and
|
||||||
|
// users navigate from another challenge
|
||||||
|
MathJax.Hub.Config({
|
||||||
|
tex2jax: {
|
||||||
|
inlineMath: [
|
||||||
|
['$', '$'],
|
||||||
|
['\\(', '\\)']
|
||||||
|
],
|
||||||
|
processEscapes: true,
|
||||||
|
processClass: 'rosetta-code|project-euler'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MathJax.Hub.Queue([
|
||||||
|
'Typeset',
|
||||||
|
MathJax.Hub,
|
||||||
|
document.querySelector('.rosetta-code'),
|
||||||
|
document.querySelector('.project-euler')
|
||||||
|
]);
|
||||||
|
} else if (!mathJaxMountPoint && mathJaxChallenge) {
|
||||||
|
mathJaxScriptLoader();
|
||||||
|
}
|
||||||
|
}, [block]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='instructions-panel'
|
||||||
|
ref={instructionsPanelRef}
|
||||||
|
role='complementary'
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{challengeTitle}
|
||||||
|
{challengeDescription}
|
||||||
|
{/* @ts-expect-error ToolPanel's redux props are being inferred here, but we don't need to provide them here */}
|
||||||
|
{showToolPanel && <ToolPanel guideUrl={guideUrl} videoUrl={videoUrl} />}
|
||||||
|
<TestSuite tests={tests} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SidePanel.displayName = 'SidePanel';
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(SidePanel);
|
64
client/src/templates/Challenges/components/video-modal.tsx
Normal file
64
client/src/templates/Challenges/components/video-modal.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||||
|
import React from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { executeGA } from '../../../redux';
|
||||||
|
import { closeModal, isVideoModalOpenSelector } from '../redux';
|
||||||
|
|
||||||
|
import './video-modal.css';
|
||||||
|
|
||||||
|
interface VideoModalProps {
|
||||||
|
closeVideoModal: () => void;
|
||||||
|
executeGA: (attributes: { type: string; data: string }) => void;
|
||||||
|
isOpen?: boolean;
|
||||||
|
t: (attribute: string) => string;
|
||||||
|
videoUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: unknown) => ({
|
||||||
|
isOpen: isVideoModalOpenSelector(state) as boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
|
bindActionCreators(
|
||||||
|
{ closeVideoModal: () => closeModal('video'), executeGA },
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
|
||||||
|
export function VideoModal({
|
||||||
|
closeVideoModal,
|
||||||
|
executeGA,
|
||||||
|
isOpen,
|
||||||
|
t,
|
||||||
|
videoUrl
|
||||||
|
}: VideoModalProps): JSX.Element {
|
||||||
|
if (isOpen) {
|
||||||
|
executeGA({ type: 'modal', data: '/completion-modal' });
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal dialogClassName='video-modal' onHide={closeVideoModal} show={isOpen}>
|
||||||
|
<Modal.Header className='video-modal-header fcc-modal' closeButton={true}>
|
||||||
|
<Modal.Title className='text-center'>
|
||||||
|
{t('buttons.watch-video')}
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body className='video-modal-body'>
|
||||||
|
<iframe
|
||||||
|
frameBorder='0'
|
||||||
|
src={videoUrl}
|
||||||
|
title={t('buttons.watch-video')}
|
||||||
|
/>
|
||||||
|
<p>{t('learn.scrimba-tip')}</p>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoModal.displayName = 'VideoModal';
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(withTranslation()(VideoModal));
|
@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
// Package Utilities
|
|
||||||
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
import { Col, Grid, Row } from '@freecodecamp/react-bootstrap';
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
@ -10,28 +10,26 @@ import { TFunction, withTranslation } from 'react-i18next';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
// Local Utilities
|
|
||||||
import Spacer from '../../../../components/helpers/spacer';
|
import Spacer from '../../../../components/helpers/spacer';
|
||||||
import LearnLayout from '../../../../components/layouts/learn';
|
import LearnLayout from '../../../../components/layouts/learn';
|
||||||
import { isSignedInSelector } from '../../../../redux';
|
import { isSignedInSelector } from '../../../../redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChallengeNodeType,
|
|
||||||
ChallengeMetaType,
|
ChallengeMetaType,
|
||||||
|
ChallengeNodeType,
|
||||||
Test
|
Test
|
||||||
} from '../../../../redux/prop-types';
|
} from '../../../../redux/prop-types';
|
||||||
import ChallengeDescription from '../../components/Challenge-Description';
|
import ChallengeDescription from '../../components/Challenge-Description';
|
||||||
import HelpModal from '../../components/HelpModal';
|
|
||||||
import Hotkeys from '../../components/Hotkeys';
|
import Hotkeys from '../../components/Hotkeys';
|
||||||
import TestSuite from '../../components/Test-Suite';
|
import TestSuite from '../../components/Test-Suite';
|
||||||
import ChallengeTitle from '../../components/challenge-title';
|
import ChallengeTitle from '../../components/challenge-title';
|
||||||
import CompletionModal from '../../components/completion-modal';
|
import CompletionModal from '../../components/completion-modal';
|
||||||
|
import HelpModal from '../../components/help-modal';
|
||||||
import Output from '../../components/output';
|
import Output from '../../components/output';
|
||||||
import {
|
import {
|
||||||
executeChallenge,
|
|
||||||
challengeMounted,
|
challengeMounted,
|
||||||
challengeTestsSelector,
|
challengeTestsSelector,
|
||||||
consoleOutputSelector,
|
consoleOutputSelector,
|
||||||
|
executeChallenge,
|
||||||
initConsole,
|
initConsole,
|
||||||
initTests,
|
initTests,
|
||||||
isChallengeCompletedSelector,
|
isChallengeCompletedSelector,
|
||||||
@ -42,7 +40,6 @@ import { getGuideUrl } from '../../utils';
|
|||||||
import SolutionForm from '../solution-form';
|
import SolutionForm from '../solution-form';
|
||||||
import ProjectToolPanel from '../tool-panel';
|
import ProjectToolPanel from '../tool-panel';
|
||||||
|
|
||||||
// Styles
|
|
||||||
import '../../components/test-frame.css';
|
import '../../components/test-frame.css';
|
||||||
|
|
||||||
// Redux Setup
|
// Redux Setup
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
// Package Utilities
|
|
||||||
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
@ -11,7 +10,6 @@ import { bindActionCreators } from 'redux';
|
|||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
// Local Utilities
|
|
||||||
import Spacer from '../../../../components/helpers/spacer';
|
import Spacer from '../../../../components/helpers/spacer';
|
||||||
import LearnLayout from '../../../../components/layouts/learn';
|
import LearnLayout from '../../../../components/layouts/learn';
|
||||||
import {
|
import {
|
||||||
@ -19,10 +17,10 @@ import {
|
|||||||
ChallengeMetaType
|
ChallengeMetaType
|
||||||
} from '../../../../redux/prop-types';
|
} from '../../../../redux/prop-types';
|
||||||
import ChallengeDescription from '../../components/Challenge-Description';
|
import ChallengeDescription from '../../components/Challenge-Description';
|
||||||
import HelpModal from '../../components/HelpModal';
|
|
||||||
import Hotkeys from '../../components/Hotkeys';
|
import Hotkeys from '../../components/Hotkeys';
|
||||||
import ChallengeTitle from '../../components/challenge-title';
|
import ChallengeTitle from '../../components/challenge-title';
|
||||||
import CompletionModal from '../../components/completion-modal';
|
import CompletionModal from '../../components/completion-modal';
|
||||||
|
import HelpModal from '../../components/help-modal';
|
||||||
import {
|
import {
|
||||||
challengeMounted,
|
challengeMounted,
|
||||||
isChallengeCompletedSelector,
|
isChallengeCompletedSelector,
|
||||||
|
@ -9,7 +9,7 @@ const backend = path.resolve(
|
|||||||
);
|
);
|
||||||
const classic = path.resolve(
|
const classic = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../src/templates/Challenges/classic/Show.tsx'
|
'../../src/templates/Challenges/classic/show.tsx'
|
||||||
);
|
);
|
||||||
const frontend = path.resolve(
|
const frontend = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
Reference in New Issue
Block a user