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;
|
||||
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 React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -8,49 +7,47 @@ import { HandlerProps } from 'react-reflex';
|
||||
import Media from 'react-responsive';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
|
||||
// Local Utilities
|
||||
import store from 'store';
|
||||
import { challengeTypes } from '../../../../utils/challenge-types';
|
||||
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import {
|
||||
ChallengeNodeType,
|
||||
ChallengeFiles,
|
||||
ChallengeFile,
|
||||
ChallengeFiles,
|
||||
ChallengeMetaType,
|
||||
Test,
|
||||
ResizePropsType
|
||||
ChallengeNodeType,
|
||||
ResizePropsType,
|
||||
Test
|
||||
} from '../../../redux/prop-types';
|
||||
import { isContained } from '../../../utils/is-contained';
|
||||
import ChallengeDescription from '../components/Challenge-Description';
|
||||
import HelpModal from '../components/HelpModal';
|
||||
import Hotkeys from '../components/Hotkeys';
|
||||
import Preview from '../components/Preview';
|
||||
import ResetModal from '../components/ResetModal';
|
||||
import SidePanel from '../components/Side-Panel';
|
||||
import VideoModal from '../components/VideoModal';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import Output from '../components/output';
|
||||
import Preview from '../components/preview';
|
||||
import SidePanel from '../components/side-panel';
|
||||
import VideoModal from '../components/video-modal';
|
||||
import {
|
||||
createFiles,
|
||||
cancelTests,
|
||||
challengeFilesSelector,
|
||||
challengeMounted,
|
||||
challengeTestsSelector,
|
||||
consoleOutputSelector,
|
||||
createFiles,
|
||||
executeChallenge,
|
||||
initConsole,
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
consoleOutputSelector,
|
||||
executeChallenge,
|
||||
cancelTests,
|
||||
isChallengeCompletedSelector
|
||||
isChallengeCompletedSelector,
|
||||
updateChallengeMeta
|
||||
} from '../redux';
|
||||
import { getGuideUrl } from '../utils';
|
||||
import DesktopLayout from './DesktopLayout';
|
||||
import MobileLayout from './MobileLayout';
|
||||
import MultifileEditor from './MultifileEditor';
|
||||
|
||||
// Styles
|
||||
import './classic.css';
|
||||
import '../components/test-frame.css';
|
||||
|
||||
@ -124,7 +121,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
static displayName: string;
|
||||
containerRef: React.RefObject<unknown>;
|
||||
editorRef: React.RefObject<unknown>;
|
||||
instructionsPanelRef: React.RefObject<HTMLElement>;
|
||||
instructionsPanelRef: React.RefObject<HTMLDivElement>;
|
||||
resizeProps: ResizePropsType;
|
||||
|
||||
constructor(props: ShowClassicProps) {
|
||||
@ -309,7 +306,6 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
}
|
||||
className='full-height'
|
||||
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||
instructionsPanelRef={this.instructionsPanelRef}
|
||||
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-call */
|
||||
/* 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 React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -10,28 +10,26 @@ import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
// Local Utilities
|
||||
import Spacer from '../../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../../components/layouts/learn';
|
||||
import { isSignedInSelector } from '../../../../redux';
|
||||
|
||||
import {
|
||||
ChallengeNodeType,
|
||||
ChallengeMetaType,
|
||||
ChallengeNodeType,
|
||||
Test
|
||||
} from '../../../../redux/prop-types';
|
||||
import ChallengeDescription from '../../components/Challenge-Description';
|
||||
import HelpModal from '../../components/HelpModal';
|
||||
import Hotkeys from '../../components/Hotkeys';
|
||||
import TestSuite from '../../components/Test-Suite';
|
||||
import ChallengeTitle from '../../components/challenge-title';
|
||||
import CompletionModal from '../../components/completion-modal';
|
||||
import HelpModal from '../../components/help-modal';
|
||||
import Output from '../../components/output';
|
||||
import {
|
||||
executeChallenge,
|
||||
challengeMounted,
|
||||
challengeTestsSelector,
|
||||
consoleOutputSelector,
|
||||
executeChallenge,
|
||||
initConsole,
|
||||
initTests,
|
||||
isChallengeCompletedSelector,
|
||||
@ -42,7 +40,6 @@ import { getGuideUrl } from '../../utils';
|
||||
import SolutionForm from '../solution-form';
|
||||
import ProjectToolPanel from '../tool-panel';
|
||||
|
||||
// Styles
|
||||
import '../../components/test-frame.css';
|
||||
|
||||
// Redux Setup
|
||||
|
@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
// Package Utilities
|
||||
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
import { graphql } from 'gatsby';
|
||||
import React, { Component } from 'react';
|
||||
@ -11,7 +10,6 @@ import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
// Local Utilities
|
||||
import Spacer from '../../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../../components/layouts/learn';
|
||||
import {
|
||||
@ -19,10 +17,10 @@ import {
|
||||
ChallengeMetaType
|
||||
} from '../../../../redux/prop-types';
|
||||
import ChallengeDescription from '../../components/Challenge-Description';
|
||||
import HelpModal from '../../components/HelpModal';
|
||||
import Hotkeys from '../../components/Hotkeys';
|
||||
import ChallengeTitle from '../../components/challenge-title';
|
||||
import CompletionModal from '../../components/completion-modal';
|
||||
import HelpModal from '../../components/help-modal';
|
||||
import {
|
||||
challengeMounted,
|
||||
isChallengeCompletedSelector,
|
||||
|
@ -9,7 +9,7 @@ const backend = path.resolve(
|
||||
);
|
||||
const classic = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/classic/Show.tsx'
|
||||
'../../src/templates/Challenges/classic/show.tsx'
|
||||
);
|
||||
const frontend = path.resolve(
|
||||
__dirname,
|
||||
|
Reference in New Issue
Block a user