feat(client): ts-migrate challenge templates/show components (#42553)

This commit is contained in:
Tom
2021-06-25 09:59:33 -05:00
committed by Mrugesh Mohapatra
parent 40323aef6a
commit 918d5a160d
10 changed files with 343 additions and 227 deletions

View File

@ -160,7 +160,9 @@ export type ChallengeNodeType = {
fields: { fields: {
slug: string; slug: string;
blockName: string; blockName: string;
tests: TestType[];
}; };
files: ChallengeFilesType;
forumTopicId: number; forumTopicId: number;
guideUrl: string; guideUrl: string;
head: string[]; head: string[];
@ -171,6 +173,11 @@ export type ChallengeNodeType = {
isLocked: boolean; isLocked: boolean;
isPrivate: boolean; isPrivate: boolean;
order: number; order: number;
question: {
text: string;
answers: string[];
solution: number;
};
required: [ required: [
{ {
link: string; link: string;
@ -184,6 +191,8 @@ export type ChallengeNodeType = {
time: string; time: string;
title: string; title: string;
translationPending: boolean; translationPending: boolean;
url: string;
videoId: string;
videoUrl: string; videoUrl: string;
}; };
@ -204,7 +213,7 @@ export type AllMarkdownRemarkType = {
}; };
export type ResizePropsType = { export type ResizePropsType = {
onStopResize: () => void; onStopResize: (arg0: React.ChangeEvent) => void;
onResize: () => void; onResize: () => void;
}; };
@ -289,6 +298,28 @@ export type ChallengeFileType = {
export type ExtTypes = 'js' | 'html' | 'css' | 'jsx'; export type ExtTypes = 'js' | 'html' | 'css' | 'jsx';
export type FileKeyTypes = 'indexjs' | 'indexhtml' | 'indexcss'; export type FileKeyTypes = 'indexjs' | 'indexhtml' | 'indexcss';
export type ChallengeFilesType =
| {
indexcss: ChallengeFileType;
indexhtml: ChallengeFileType;
indexjs: ChallengeFileType;
indexjsx: ChallengeFileType;
}
| Record<string, never>;
export type ChallengeMetaType = {
block: string;
id: string;
introPath: string;
nextChallengePath: string;
prevChallengePath: string;
removeComments: boolean;
superBlock: string;
title?: string;
challengeType?: number;
helpCategory: string;
};
export type PortfolioType = { export type PortfolioType = {
id: string; id: string;
title?: string; title?: string;
@ -297,6 +328,7 @@ export type PortfolioType = {
description?: string; description?: string;
}; };
// This looks redundant - same as ChallengeNodeType above?
export type ChallengeNode = { export type ChallengeNode = {
block: string; block: string;
challengeOrder: number; challengeOrder: number;

View File

@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// Package Utilities
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import { bindActionCreators, Dispatch } from 'redux';
import { bindActionCreators } from 'redux';
import { createStructuredSelector } from 'reselect'; import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
@ -8,6 +11,7 @@ import { graphql } from 'gatsby';
import Media from 'react-responsive'; import Media from 'react-responsive';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
// Local Utilities
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
import MultifileEditor from './MultifileEditor'; import MultifileEditor from './MultifileEditor';
import Preview from '../components/Preview'; import Preview from '../components/Preview';
@ -20,12 +24,17 @@ import ResetModal from '../components/ResetModal';
import MobileLayout from './MobileLayout'; import MobileLayout from './MobileLayout';
import DesktopLayout from './DesktopLayout'; import DesktopLayout from './DesktopLayout';
import Hotkeys from '../components/Hotkeys'; import Hotkeys from '../components/Hotkeys';
import { getGuideUrl } from '../utils'; import { getGuideUrl } from '../utils';
import store from 'store'; import store from 'store';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challengeTypes';
import { isContained } from '../../../utils/is-contained'; import { isContained } from '../../../utils/is-contained';
import { ChallengeNode } from '../../../redux/prop-types'; import {
ChallengeNodeType,
ChallengeFilesType,
ChallengeMetaType,
TestType,
ResizePropsType
} from '../../../redux/prop-types';
import { import {
createFiles, createFiles,
challengeFilesSelector, challengeFilesSelector,
@ -39,16 +48,18 @@ import {
cancelTests cancelTests
} from '../redux'; } from '../redux';
// Styles
import './classic.css'; import './classic.css';
import '../components/test-frame.css'; import '../components/test-frame.css';
// Redux Setup
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
files: challengeFilesSelector, files: challengeFilesSelector,
tests: challengeTestsSelector, tests: challengeTestsSelector,
output: consoleOutputSelector output: consoleOutputSelector
}); });
const mapDispatchToProps = dispatch => const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators( bindActionCreators(
{ {
createFiles, createFiles,
@ -62,60 +73,58 @@ const mapDispatchToProps = dispatch =>
dispatch dispatch
); );
const propTypes = { // Types
cancelTests: PropTypes.func.isRequired, interface ShowClassicProps {
challengeMounted: PropTypes.func.isRequired, cancelTests: () => void;
createFiles: PropTypes.func.isRequired, challengeMounted: (arg0: string) => void;
data: PropTypes.shape({ createFiles: (arg0: ChallengeFilesType) => void;
challengeNode: ChallengeNode data: { challengeNode: ChallengeNodeType };
}), executeChallenge: () => void;
executeChallenge: PropTypes.func.isRequired, files: ChallengeFilesType;
files: PropTypes.shape({ initConsole: (arg0: string) => void;
key: PropTypes.string initTests: (tests: TestType[]) => void;
}), output: string[];
initConsole: PropTypes.func.isRequired, pageContext: {
initTests: PropTypes.func.isRequired, challengeMeta: ChallengeMetaType;
output: PropTypes.arrayOf(PropTypes.string), };
pageContext: PropTypes.shape({ t: (arg0: string) => string;
challengeMeta: PropTypes.shape({ tests: TestType[];
id: PropTypes.string, updateChallengeMeta: (arg0: ChallengeMetaType) => void;
nextChallengePath: PropTypes.string, }
prevChallengePath: PropTypes.string
}) interface ShowClassicState {
}), resizing: boolean;
t: PropTypes.func.isRequired, }
tests: PropTypes.arrayOf(
PropTypes.shape({ interface IReflexLayout {
text: PropTypes.string, codePane: { flex: number };
testString: PropTypes.string editorPane: { flex: number };
}) instructionPane: { flex: number };
), previewPane: { flex: number };
updateChallengeMeta: PropTypes.func.isRequired testsPane: { flex: number };
}; }
const MAX_MOBILE_WIDTH = 767; const MAX_MOBILE_WIDTH = 767;
const REFLEX_LAYOUT = 'challenge-layout'; const REFLEX_LAYOUT = 'challenge-layout';
const BASE_LAYOUT = { const BASE_LAYOUT = {
codePane: { codePane: { flex: 1 },
flex: 1 editorPane: { flex: 1 },
}, instructionPane: { flex: 1 },
editorPane: { previewPane: { flex: 0.7 },
flex: 1 testsPane: { flex: 0.25 }
},
instructionPane: {
flex: 1
},
previewPane: {
flex: 0.7
},
testsPane: {
flex: 0.25
}
}; };
class ShowClassic extends Component { // Component
constructor() { class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
super(); static displayName: string;
containerRef: React.RefObject<unknown>;
editorRef: React.RefObject<unknown>;
resizeProps: ResizePropsType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layoutState: any;
constructor(props: ShowClassicProps) {
super(props);
this.resizeProps = { this.resizeProps = {
onStopResize: this.onStopResize.bind(this), onStopResize: this.onStopResize.bind(this),
@ -132,8 +141,8 @@ class ShowClassic extends Component {
this.layoutState = this.getLayoutState(); this.layoutState = this.getLayoutState();
} }
getLayoutState() { getLayoutState(): IReflexLayout | string {
const reflexLayout = store.get(REFLEX_LAYOUT); const reflexLayout: IReflexLayout | string = store.get(REFLEX_LAYOUT);
// Validate if user has not done any resize of the panes // Validate if user has not done any resize of the panes
if (!reflexLayout) return BASE_LAYOUT; if (!reflexLayout) return BASE_LAYOUT;
@ -153,7 +162,8 @@ class ShowClassic extends Component {
this.setState({ resizing: true }); this.setState({ resizing: true });
} }
onStopResize(event) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
onStopResize(event: any) {
const { name, flex } = event.component.props; const { name, flex } = event.component.props;
this.setState({ resizing: false }); this.setState({ resizing: false });
@ -177,7 +187,7 @@ class ShowClassic extends Component {
this.initializeComponent(title); this.initializeComponent(title);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: ShowClassicProps) {
const { const {
data: { data: {
challengeNode: { challengeNode: {
@ -199,7 +209,7 @@ class ShowClassic extends Component {
} }
} }
initializeComponent(title) { initializeComponent(title: string) {
const { const {
challengeMounted, challengeMounted,
createFiles, createFiles,
@ -256,7 +266,7 @@ class ShowClassic extends Component {
); );
} }
renderInstructionsPanel({ showToolPanel }) { renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) {
const { block, description, instructions, superBlock, translationPending } = const { block, description, instructions, superBlock, translationPending } =
this.getChallenge(); this.getChallenge();
@ -397,7 +407,6 @@ class ShowClassic extends Component {
} }
ShowClassic.displayName = 'ShowClassic'; ShowClassic.displayName = 'ShowClassic';
ShowClassic.propTypes = propTypes;
export default connect( export default connect(
mapStateToProps, mapStateToProps,

View File

@ -2,20 +2,23 @@
// Package Utilities // Package Utilities
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import type { Dispatch } from 'redux';
// Local Utilities // Local Utilities
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
import { ChallengeNode } from '../../../redux/prop-types'; import {
ChallengeNodeType,
ChallengeMetaType
} from '../../../redux/prop-types';
import { updateChallengeMeta, challengeMounted } from '../redux'; import { updateChallengeMeta, challengeMounted } from '../redux';
// Redux // Redux
const mapStateToProps = () => ({}); const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch => const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators( bindActionCreators(
{ {
updateChallengeMeta, updateChallengeMeta,
@ -24,25 +27,19 @@ const mapDispatchToProps = dispatch =>
dispatch dispatch
); );
// Proptypes // Types
const propTypes = { interface ShowCodeAllyProps {
data: PropTypes.shape({ data: { challengeNode: ChallengeNodeType };
challengeNode: ChallengeNode pageContext: {
}), challengeMeta: ChallengeMetaType;
pageContext: PropTypes.shape({ };
challengeMeta: PropTypes.shape({ updateChallengeMeta: (arg0: ChallengeMetaType) => void;
id: PropTypes.string, }
introPath: PropTypes.string,
nextChallengePath: PropTypes.string,
prevChallengePath: PropTypes.string
})
}),
updateChallengeMeta: PropTypes.func.isRequired
};
// Component // Component
class ShowCodeAlly extends Component { class ShowCodeAlly extends Component<ShowCodeAllyProps> {
componentDidMount() { static displayName: string;
componentDidMount(): void {
const { const {
updateChallengeMeta, updateChallengeMeta,
data: { data: {
@ -65,6 +62,7 @@ class ShowCodeAlly extends Component {
<Helmet title={`${blockName}: ${title} | freeCodeCamp.org`} /> <Helmet title={`${blockName}: ${title} | freeCodeCamp.org`} />
<iframe <iframe
sandbox='allow-modals allow-forms allow-popups allow-scripts allow-same-origin' sandbox='allow-modals allow-forms allow-popups allow-scripts allow-same-origin'
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
src={`http://codeally.io/embed/?repoUrl=${url}`} src={`http://codeally.io/embed/?repoUrl=${url}`}
style={{ style={{
width: '100%', width: '100%',
@ -80,7 +78,6 @@ class ShowCodeAlly extends Component {
} }
ShowCodeAlly.displayName = 'ShowCodeAlly'; ShowCodeAlly.displayName = 'ShowCodeAlly';
ShowCodeAlly.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(ShowCodeAlly); export default connect(mapStateToProps, mapDispatchToProps)(ShowCodeAlly);

View File

@ -4,9 +4,9 @@ import PrismFormatted from './PrismFormatted';
import './challenge-description.css'; import './challenge-description.css';
type Challenge = { type Challenge = {
block: string; block?: string;
description: string; description?: string;
instructions: string; instructions?: string;
}; };
function ChallengeDescription(challenge: Challenge): JSX.Element { function ChallengeDescription(challenge: Challenge): JSX.Element {

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { HotKeys, GlobalHotKeys } from 'react-hotkeys'; import { HotKeys, GlobalHotKeys } from 'react-hotkeys';
import { navigate } from 'gatsby'; import { navigate } from 'gatsby';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -10,7 +11,7 @@ import './hotkeys.css';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
canFocusEditorSelector, canFocusEditorSelector,
canFocusEditor => ({ (canFocusEditor: boolean) => ({
canFocusEditor canFocusEditor
}) })
); );
@ -25,16 +26,17 @@ const keyMap = {
NAVIGATE_NEXT: ['n'] NAVIGATE_NEXT: ['n']
}; };
const propTypes = { interface HotkeysProps {
canFocusEditor: PropTypes.bool, canFocusEditor: boolean;
children: PropTypes.any, children: React.ReactElement;
editorRef: PropTypes.object, // eslint-disable-next-line @typescript-eslint/no-explicit-any
executeChallenge: PropTypes.func, editorRef?: React.Ref<HTMLElement> | any;
innerRef: PropTypes.any, executeChallenge?: () => void;
nextChallengePath: PropTypes.string, innerRef: React.Ref<HTMLElement> | unknown;
prevChallengePath: PropTypes.string, nextChallengePath: string;
setEditorFocusability: PropTypes.func.isRequired prevChallengePath: string;
}; setEditorFocusability: (arg0: boolean) => void;
}
function Hotkeys({ function Hotkeys({
canFocusEditor, canFocusEditor,
@ -45,9 +47,9 @@ function Hotkeys({
nextChallengePath, nextChallengePath,
prevChallengePath, prevChallengePath,
setEditorFocusability setEditorFocusability
}) { }: HotkeysProps): JSX.Element {
const handlers = { const handlers = {
EXECUTE_CHALLENGE: e => { EXECUTE_CHALLENGE: (e: React.KeyboardEvent<HTMLButtonElement>) => {
// the 'enter' part of 'ctrl+enter' stops HotKeys from listening, so it // the 'enter' part of 'ctrl+enter' stops HotKeys from listening, so it
// needs to be prevented. // needs to be prevented.
// TODO: 'enter' on its own also disables HotKeys, but default behaviour // TODO: 'enter' on its own also disables HotKeys, but default behaviour
@ -55,7 +57,7 @@ function Hotkeys({
e.preventDefault(); e.preventDefault();
if (executeChallenge) executeChallenge(); if (executeChallenge) executeChallenge();
}, },
FOCUS_EDITOR: e => { FOCUS_EDITOR: (e: React.KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
if (editorRef && editorRef.current) { if (editorRef && editorRef.current) {
editorRef.current.getWrappedInstance().focusOnEditor(); editorRef.current.getWrappedInstance().focusOnEditor();
@ -63,10 +65,10 @@ function Hotkeys({
}, },
NAVIGATION_MODE: () => setEditorFocusability(false), NAVIGATION_MODE: () => setEditorFocusability(false),
NAVIGATE_PREV: () => { NAVIGATE_PREV: () => {
if (!canFocusEditor) navigate(prevChallengePath); if (!canFocusEditor) void navigate(prevChallengePath);
}, },
NAVIGATE_NEXT: () => { NAVIGATE_NEXT: () => {
if (!canFocusEditor) navigate(nextChallengePath); if (!canFocusEditor) void navigate(nextChallengePath);
} }
}; };
// GlobalHotKeys is always mounted and tracks all keypresses. Without it, // GlobalHotKeys is always mounted and tracks all keypresses. Without it,
@ -75,19 +77,22 @@ function Hotkeys({
// allowChanges is necessary if the handlers depend on props (in this case // allowChanges is necessary if the handlers depend on props (in this case
// canFocusEditor) // canFocusEditor)
return ( return (
<HotKeys <>
allowChanges={true} {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
handlers={handlers} {/* @ts-ignore */}
innerRef={innerRef} <HotKeys
keyMap={keyMap} allowChanges={true}
> handlers={handlers}
{children} innerRef={innerRef}
<GlobalHotKeys /> keyMap={keyMap}
</HotKeys> >
{children}
<GlobalHotKeys />
</HotKeys>
</>
); );
} }
Hotkeys.displayName = 'Hotkeys'; Hotkeys.displayName = 'Hotkeys';
Hotkeys.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(Hotkeys); export default connect(mapStateToProps, mapDispatchToProps)(Hotkeys);

View File

@ -1,28 +1,35 @@
// Package Utilities
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { bindActionCreators, Dispatch } from 'redux';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap'; import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
// Local Utilities
import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux'; import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux';
import { executeGA } from '../../../redux'; import { executeGA } from '../../../redux';
// Styles
import './reset-modal.css'; import './reset-modal.css';
const propTypes = { // Types
close: PropTypes.func.isRequired, interface ResetModalProps {
executeGA: PropTypes.func, close: () => void;
isOpen: PropTypes.bool.isRequired, executeGA: () => void;
reset: PropTypes.func.isRequired isOpen: boolean;
}; reset: () => void;
}
const mapStateToProps = createSelector(isResetModalOpenSelector, isOpen => ({ // Redux Setup
isOpen const mapStateToProps = createSelector(
})); isResetModalOpenSelector,
(isOpen: boolean) => ({
isOpen
})
);
const mapDispatchToProps = dispatch => const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators( bindActionCreators(
{ {
close: () => closeModal('reset'), close: () => closeModal('reset'),
@ -32,11 +39,12 @@ const mapDispatchToProps = dispatch =>
dispatch dispatch
); );
function withActions(...fns) { function withActions(...fns: Array<() => void>) {
return () => fns.forEach(fn => fn()); return () => fns.forEach(fn => fn());
} }
function ResetModal({ reset, close, isOpen }) { // Component
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
if (isOpen) { if (isOpen) {
executeGA({ type: 'modal', data: '/reset-modal' }); executeGA({ type: 'modal', data: '/reset-modal' });
@ -75,6 +83,5 @@ function ResetModal({ reset, close, isOpen }) {
} }
ResetModal.displayName = 'ResetModal'; ResetModal.displayName = 'ResetModal';
ResetModal.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(ResetModal); export default connect(mapStateToProps, mapDispatchToProps)(ResetModal);

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// Package Utilities
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap'; import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -7,6 +9,7 @@ import { graphql } from 'gatsby';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
// Local Utilities
import { import {
executeChallenge, executeChallenge,
challengeMounted, challengeMounted,
@ -19,7 +22,6 @@ import {
updateSolutionFormValues updateSolutionFormValues
} from '../../redux'; } from '../../redux';
import { getGuideUrl } from '../../utils'; import { getGuideUrl } from '../../utils';
import LearnLayout from '../../../../components/layouts/Learn'; import LearnLayout from '../../../../components/layouts/Learn';
import ChallengeTitle from '../../components/Challenge-Title'; import ChallengeTitle from '../../components/Challenge-Title';
import ChallengeDescription from '../../components/Challenge-Description'; import ChallengeDescription from '../../components/Challenge-Description';
@ -30,42 +32,29 @@ import HelpModal from '../../components/HelpModal';
import ProjectToolPanel from '../Tool-Panel'; import ProjectToolPanel from '../Tool-Panel';
import SolutionForm from '../SolutionForm'; import SolutionForm from '../SolutionForm';
import Spacer from '../../../../components/helpers/spacer'; import Spacer from '../../../../components/helpers/spacer';
import { ChallengeNode } from '../../../../redux/prop-types'; import {
ChallengeNodeType,
ChallengeMetaType,
TestType
} from '../../../../redux/prop-types';
import { isSignedInSelector } from '../../../../redux'; import { isSignedInSelector } from '../../../../redux';
import Hotkeys from '../../components/Hotkeys'; import Hotkeys from '../../components/Hotkeys';
// Styles
import '../../components/test-frame.css'; import '../../components/test-frame.css';
const propTypes = { // Redux Setup
challengeMounted: PropTypes.func.isRequired,
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
description: PropTypes.string,
executeChallenge: PropTypes.func.isRequired,
forumTopicId: PropTypes.number,
id: PropTypes.string,
initConsole: PropTypes.func.isRequired,
initTests: PropTypes.func.isRequired,
isChallengeCompleted: PropTypes.bool,
isSignedIn: PropTypes.bool,
output: PropTypes.arrayOf(PropTypes.string),
pageContext: PropTypes.shape({
challengeMeta: PropTypes.object
}),
t: PropTypes.func.isRequired,
tests: PropTypes.array,
title: PropTypes.string,
updateChallengeMeta: PropTypes.func.isRequired,
updateSolutionFormValues: PropTypes.func.isRequired
};
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
consoleOutputSelector, consoleOutputSelector,
challengeTestsSelector, challengeTestsSelector,
isChallengeCompletedSelector, isChallengeCompletedSelector,
isSignedInSelector, isSignedInSelector,
(output, tests, isChallengeCompleted, isSignedIn) => ({ (
output: string[],
tests: TestType[],
isChallengeCompleted: boolean,
isSignedIn: boolean
) => ({
tests, tests,
output, output,
isChallengeCompleted, isChallengeCompleted,
@ -82,8 +71,35 @@ const mapDispatchToActions = {
updateSolutionFormValues updateSolutionFormValues
}; };
class BackEnd extends Component { // Types
constructor(props) { interface BackEndProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNodeType };
description: string;
executeChallenge: (arg0: boolean) => void;
forumTopicId: number;
id: string;
initConsole: () => void;
initTests: (tests: TestType[]) => void;
isChallengeCompleted: boolean;
isSignedIn: boolean;
output: string[];
pageContext: {
challengeMeta: ChallengeMetaType;
};
t: (arg0: string) => string;
tests: TestType[];
title: string;
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
updateSolutionFormValues: () => void;
}
// Component
class BackEnd extends Component<BackEndProps> {
static displayName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _container: any;
constructor(props: BackEndProps) {
super(props); super(props);
this.state = {}; this.state = {};
this.updateDimensions = this.updateDimensions.bind(this); this.updateDimensions = this.updateDimensions.bind(this);
@ -92,7 +108,7 @@ class BackEnd extends Component {
componentDidMount() { componentDidMount() {
this.initializeComponent(); this.initializeComponent();
window.addEventListener('resize', this.updateDimensions); window.addEventListener('resize', () => this.updateDimensions());
this._container.focus(); this._container.focus();
} }
@ -101,10 +117,10 @@ class BackEnd extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.updateDimensions); window.removeEventListener('resize', () => this.updateDimensions());
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: BackEndProps) {
const { const {
data: { data: {
challengeNode: { challengeNode: {
@ -122,7 +138,7 @@ class BackEnd extends Component {
} }
} = this.props; } = this.props;
if (prevTitle !== currentTitle || prevTests !== currTests) { if (prevTitle !== currentTitle || prevTests !== currTests) {
this.initializeComponent(currentTitle); this.initializeComponent();
} }
} }
@ -153,7 +169,11 @@ class BackEnd extends Component {
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
} }
handleSubmit({ isShouldCompletionModalOpen }) { handleSubmit({
isShouldCompletionModalOpen
}: {
isShouldCompletionModalOpen: boolean;
}): void {
this.props.executeChallenge(isShouldCompletionModalOpen); this.props.executeChallenge(isShouldCompletionModalOpen);
} }
@ -186,7 +206,7 @@ class BackEnd extends Component {
return ( return (
<Hotkeys <Hotkeys
innerRef={c => (this._container = c)} innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath} nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath} prevChallengePath={prevChallengePath}
> >
@ -197,6 +217,8 @@ class BackEnd extends Component {
<Grid> <Grid>
<Row> <Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}> <Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
<ChallengeTitle <ChallengeTitle
block={block} block={block}
@ -212,6 +234,7 @@ class BackEnd extends Component {
/> />
<SolutionForm <SolutionForm
challengeType={challengeType} challengeType={challengeType}
// eslint-disable-next-line @typescript-eslint/unbound-method
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
updateSolutionForm={updateSolutionFormValues} updateSolutionForm={updateSolutionFormValues}
/> />
@ -231,6 +254,8 @@ class BackEnd extends Component {
output={output} output={output}
/> />
<TestSuite tests={tests} /> <TestSuite tests={tests} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
</Col> </Col>
<CompletionModal <CompletionModal
@ -248,7 +273,6 @@ class BackEnd extends Component {
} }
BackEnd.displayName = 'BackEnd'; BackEnd.displayName = 'BackEnd';
BackEnd.propTypes = propTypes;
export default connect( export default connect(
mapStateToProps, mapStateToProps,

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// Package Utilities
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap'; import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -7,8 +9,13 @@ import { graphql } from 'gatsby';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import type { Dispatch } from 'redux';
import { ChallengeNode } from '../../../../redux/prop-types'; // Local Utilities
import {
ChallengeNodeType,
ChallengeMetaType
} from '../../../../redux/prop-types';
import { import {
challengeMounted, challengeMounted,
isChallengeCompletedSelector, isChallengeCompletedSelector,
@ -16,9 +23,7 @@ import {
openModal, openModal,
updateSolutionFormValues updateSolutionFormValues
} from '../../redux'; } from '../../redux';
import { getGuideUrl } from '../../utils'; import { getGuideUrl } from '../../utils';
import LearnLayout from '../../../../components/layouts/Learn'; import LearnLayout from '../../../../components/layouts/Learn';
import ChallengeTitle from '../../components/Challenge-Title'; import ChallengeTitle from '../../components/Challenge-Title';
import ChallengeDescription from '../../components/Challenge-Description'; import ChallengeDescription from '../../components/Challenge-Description';
@ -29,14 +34,15 @@ import CompletionModal from '../../components/CompletionModal';
import HelpModal from '../../components/HelpModal'; import HelpModal from '../../components/HelpModal';
import Hotkeys from '../../components/Hotkeys'; import Hotkeys from '../../components/Hotkeys';
// Redux Setup
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
isChallengeCompletedSelector, isChallengeCompletedSelector,
isChallengeCompleted => ({ (isChallengeCompleted: boolean) => ({
isChallengeCompleted isChallengeCompleted
}) })
); );
const mapDispatchToProps = dispatch => const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators( bindActionCreators(
{ {
updateChallengeMeta, updateChallengeMeta,
@ -47,24 +53,28 @@ const mapDispatchToProps = dispatch =>
dispatch dispatch
); );
const propTypes = { // Types
challengeMounted: PropTypes.func.isRequired, interface ProjectProps {
data: PropTypes.shape({ challengeMounted: (arg0: string) => void;
challengeNode: ChallengeNode data: { challengeNode: ChallengeNodeType };
}), isChallengeCompleted: boolean;
isChallengeCompleted: PropTypes.bool, openCompletionModal: () => void;
openCompletionModal: PropTypes.func.isRequired, pageContext: {
pageContext: PropTypes.shape({ challengeMeta: ChallengeMetaType;
challengeMeta: PropTypes.object };
}), t: (arg0: string) => string;
t: PropTypes.func.isRequired, updateChallengeMeta: (arg0: ChallengeMetaType) => void;
updateChallengeMeta: PropTypes.func.isRequired, updateSolutionFormValues: () => void;
updateSolutionFormValues: PropTypes.func.isRequired }
};
class Project extends Component { // Component
constructor() { class Project extends Component<ProjectProps> {
super(); static displayName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _container: any;
constructor(props: ProjectProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -86,7 +96,7 @@ class Project extends Component {
this._container.focus(); this._container.focus();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: ProjectProps): void {
const { const {
data: { data: {
challengeNode: { title: prevTitle } challengeNode: { title: prevTitle }
@ -111,7 +121,11 @@ class Project extends Component {
} }
} }
handleSubmit({ isShouldCompletionModalOpen }) { handleSubmit({
isShouldCompletionModalOpen
}: {
isShouldCompletionModalOpen: boolean;
}): void {
if (isShouldCompletionModalOpen) { if (isShouldCompletionModalOpen) {
this.props.openCompletionModal(); this.props.openCompletionModal();
} }
@ -143,7 +157,7 @@ class Project extends Component {
return ( return (
<Hotkeys <Hotkeys
innerRef={c => (this._container = c)} innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath} nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath} prevChallengePath={prevChallengePath}
> >
@ -154,6 +168,8 @@ class Project extends Component {
<Grid> <Grid>
<Row> <Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}> <Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
<ChallengeTitle <ChallengeTitle
block={block} block={block}
@ -167,6 +183,7 @@ class Project extends Component {
<SolutionForm <SolutionForm
challengeType={challengeType} challengeType={challengeType}
description={description} description={description}
// eslint-disable-next-line @typescript-eslint/unbound-method
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
updateSolutionForm={updateSolutionFormValues} updateSolutionForm={updateSolutionFormValues}
/> />
@ -174,6 +191,8 @@ class Project extends Component {
guideUrl={getGuideUrl({ forumTopicId, title })} guideUrl={getGuideUrl({ forumTopicId, title })}
/> />
<br /> <br />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
</Col> </Col>
<CompletionModal <CompletionModal
@ -191,7 +210,6 @@ class Project extends Component {
} }
Project.displayName = 'Project'; Project.displayName = 'Project';
Project.propTypes = propTypes;
export default connect( export default connect(
mapStateToProps, mapStateToProps,

View File

@ -1,6 +1,5 @@
// Package Utilities // Package Utilities
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap'; import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -10,10 +9,14 @@ import YouTube from 'react-youtube';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { ObserveKeys } from 'react-hotkeys'; import { ObserveKeys } from 'react-hotkeys';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
// Local Utilities // Local Utilities
import PrismFormatted from '../components/PrismFormatted'; import PrismFormatted from '../components/PrismFormatted';
import { ChallengeNode } from '../../../redux/prop-types'; import {
ChallengeNodeType,
ChallengeMetaType
} from '../../../redux/prop-types';
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
import ChallengeTitle from '../components/Challenge-Title'; import ChallengeTitle from '../components/Challenge-Title';
import ChallengeDescription from '../components/Challenge-Description'; import ChallengeDescription from '../components/Challenge-Description';
@ -35,11 +38,11 @@ import './show.css';
// Redux Setup // Redux Setup
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
isChallengeCompletedSelector, isChallengeCompletedSelector,
isChallengeCompleted => ({ (isChallengeCompleted: boolean) => ({
isChallengeCompleted isChallengeCompleted
}) })
); );
const mapDispatchToProps = dispatch => const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators( bindActionCreators(
{ {
updateChallengeMeta, updateChallengeMeta,
@ -50,26 +53,36 @@ const mapDispatchToProps = dispatch =>
dispatch dispatch
); );
// Proptypes // Types
const propTypes = { interface ShowVideoProps {
challengeMounted: PropTypes.func.isRequired, challengeMounted: (arg0: string) => void;
data: PropTypes.shape({ data: { challengeNode: ChallengeNodeType };
challengeNode: ChallengeNode description: string;
}), isChallengeCompleted: boolean;
description: PropTypes.string, openCompletionModal: () => void;
isChallengeCompleted: PropTypes.bool, pageContext: {
openCompletionModal: PropTypes.func.isRequired, challengeMeta: ChallengeMetaType;
pageContext: PropTypes.shape({ };
challengeMeta: PropTypes.object t: (arg0: string) => string;
}), updateChallengeMeta: (arg0: ChallengeMetaType) => void;
t: PropTypes.func.isRequired, updateSolutionFormValues: () => void;
updateChallengeMeta: PropTypes.func.isRequired, }
updateSolutionFormValues: PropTypes.func.isRequired
}; interface ShowVideoState {
subtitles: string;
downloadURL: string | null;
selectedOption: number | null;
answer: number;
showWrong: boolean;
videoIsLoaded: boolean;
}
// Component // Component
class Project extends Component { class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
constructor(props) { static displayName: string;
private _container: HTMLElement | null | undefined;
constructor(props: ShowVideoProps) {
super(props); super(props);
this.state = { this.state = {
subtitles: '', subtitles: '',
@ -83,7 +96,7 @@ class Project extends Component {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
componentDidMount() { componentDidMount(): void {
const { const {
challengeMounted, challengeMounted,
data: { data: {
@ -99,10 +112,9 @@ class Project extends Component {
helpCategory helpCategory
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
this._container.focus();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: ShowVideoProps): void {
const { const {
data: { data: {
challengeNode: { title: prevTitle } challengeNode: { title: prevTitle }
@ -127,7 +139,7 @@ class Project extends Component {
} }
} }
handleSubmit(solution, openCompletionModal) { handleSubmit(solution: number, openCompletionModal: () => void) {
if (solution - 1 === this.state.selectedOption) { if (solution - 1 === this.state.selectedOption) {
this.setState({ this.setState({
showWrong: false showWrong: false
@ -140,7 +152,9 @@ class Project extends Component {
} }
} }
handleOptionChange = changeEvent => { handleOptionChange = (
changeEvent: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({ this.setState({
showWrong: false, showWrong: false,
selectedOption: parseInt(changeEvent.target.value, 10) selectedOption: parseInt(changeEvent.target.value, 10)
@ -181,7 +195,7 @@ class Project extends Component {
executeChallenge={() => { executeChallenge={() => {
this.handleSubmit(solution, openCompletionModal); this.handleSubmit(solution, openCompletionModal);
}} }}
innerRef={c => (this._container = c)} innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath} nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath} prevChallengePath={prevChallengePath}
> >
@ -191,6 +205,8 @@ class Project extends Component {
/> />
<Grid> <Grid>
<Row> <Row>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
<ChallengeTitle <ChallengeTitle
block={block} block={block}
@ -222,6 +238,7 @@ class Project extends Component {
width: 'auto', width: 'auto',
height: 'auto' height: 'auto'
}} }}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
videoId={videoId} videoId={videoId}
/> />
<i> <i>
@ -243,6 +260,8 @@ class Project extends Component {
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}> <Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<ChallengeDescription description={description} /> <ChallengeDescription description={description} />
<PrismFormatted className={'line-numbers'} text={text} /> <PrismFormatted className={'line-numbers'} text={text} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
<ObserveKeys> <ObserveKeys>
<div className='video-quiz-options'> <div className='video-quiz-options'>
@ -272,6 +291,8 @@ class Project extends Component {
))} ))}
</div> </div>
</ObserveKeys> </ObserveKeys>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
<div <div
style={{ style={{
@ -284,6 +305,8 @@ class Project extends Component {
<span>{t('learn.check-answer')}</span> <span>{t('learn.check-answer')}</span>
)} )}
</div> </div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer /> <Spacer />
<Button <Button
block={true} block={true}
@ -295,6 +318,8 @@ class Project extends Component {
> >
{t('buttons.check-answer')} {t('buttons.check-answer')}
</Button> </Button>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer size={2} /> <Spacer size={2} />
</Col> </Col>
<CompletionModal <CompletionModal
@ -310,13 +335,12 @@ class Project extends Component {
} }
} }
Project.displayName = 'Project'; ShowVideo.displayName = 'ShowVideo';
Project.propTypes = propTypes;
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(withTranslation()(Project)); )(withTranslation()(ShowVideo));
export const query = graphql` export const query = graphql`
query VideoChallenge($slug: String!) { query VideoChallenge($slug: String!) {

View File

@ -5,19 +5,19 @@ const { viewTypes } = require('../challengeTypes');
const backend = path.resolve( const backend = path.resolve(
__dirname, __dirname,
'../../src/templates/Challenges/projects/backend/Show.js' '../../src/templates/Challenges/projects/backend/Show.tsx'
); );
const classic = path.resolve( const classic = path.resolve(
__dirname, __dirname,
'../../src/templates/Challenges/classic/Show.js' '../../src/templates/Challenges/classic/Show.tsx'
); );
const frontend = path.resolve( const frontend = path.resolve(
__dirname, __dirname,
'../../src/templates/Challenges/projects/frontend/Show.js' '../../src/templates/Challenges/projects/frontend/Show.tsx'
); );
const codeally = path.resolve( const codeally = path.resolve(
__dirname, __dirname,
'../../src/templates/Challenges/codeally/show.js' '../../src/templates/Challenges/codeally/show.tsx'
); );
const intro = path.resolve( const intro = path.resolve(
__dirname, __dirname,
@ -29,7 +29,7 @@ const superBlockIntro = path.resolve(
); );
const video = path.resolve( const video = path.resolve(
__dirname, __dirname,
'../../src/templates/Challenges/video/Show.js' '../../src/templates/Challenges/video/Show.tsx'
); );
const views = { const views = {