diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts
index d668274d87..3bf4cc7b1b 100644
--- a/client/src/redux/prop-types.ts
+++ b/client/src/redux/prop-types.ts
@@ -167,6 +167,7 @@ export type ChallengeNodeType = {
guideUrl: string;
head: string[];
helpCategory: string;
+ id: string;
instructions: string;
isComingSoon: boolean;
removeComments: boolean;
diff --git a/client/src/templates/Challenges/classic/Show.tsx b/client/src/templates/Challenges/classic/Show.tsx
index 85dc114b50..b93e8200a5 100644
--- a/client/src/templates/Challenges/classic/Show.tsx
+++ b/client/src/templates/Challenges/classic/Show.tsx
@@ -17,7 +17,7 @@ import MultifileEditor from './MultifileEditor';
import Preview from '../components/Preview';
import SidePanel from '../components/Side-Panel';
import Output from '../components/Output';
-import CompletionModal from '../components/CompletionModal';
+import CompletionModal from '../components/completion-modal';
import HelpModal from '../components/HelpModal';
import VideoModal from '../components/VideoModal';
import ResetModal from '../components/ResetModal';
diff --git a/client/src/templates/Challenges/components/__snapshots__/CompletionModalBody.test.js.snap b/client/src/templates/Challenges/components/__snapshots__/completion-modal-body.test.tsx.snap
similarity index 100%
rename from client/src/templates/Challenges/components/__snapshots__/CompletionModalBody.test.js.snap
rename to client/src/templates/Challenges/components/__snapshots__/completion-modal-body.test.tsx.snap
diff --git a/client/src/templates/Challenges/components/CompletionModalBody.test.js b/client/src/templates/Challenges/components/completion-modal-body.test.tsx
similarity index 89%
rename from client/src/templates/Challenges/components/CompletionModalBody.test.js
rename to client/src/templates/Challenges/components/completion-modal-body.test.tsx
index 92827be419..2e69a480d1 100644
--- a/client/src/templates/Challenges/components/CompletionModalBody.test.js
+++ b/client/src/templates/Challenges/components/completion-modal-body.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
-import CompletionModalBody from './CompletionModalBody';
+import CompletionModalBody from './completion-modal-body';
const props = {
block: 'basic-html-and-html5',
@@ -33,7 +33,7 @@ describe('', () => {
const { container } = render();
fireEvent.animationEnd(
- container.querySelector('.completion-success-icon')
+ container.querySelector('.completion-success-icon') as HTMLElement
);
jest.runAllTimers();
diff --git a/client/src/templates/Challenges/components/CompletionModalBody.js b/client/src/templates/Challenges/components/completion-modal-body.tsx
similarity index 76%
rename from client/src/templates/Challenges/components/CompletionModalBody.js
rename to client/src/templates/Challenges/components/completion-modal-body.tsx
index 83847f0ca7..d01f2329c1 100644
--- a/client/src/templates/Challenges/components/CompletionModalBody.js
+++ b/client/src/templates/Challenges/components/completion-modal-body.tsx
@@ -1,18 +1,28 @@
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
import BezierEasing from 'bezier-easing';
import GreenPass from '../../../assets/icons/green-pass';
import { withTranslation } from 'react-i18next';
-const propTypes = {
- block: PropTypes.string,
- completedPercent: PropTypes.number,
- superBlock: PropTypes.string,
- t: PropTypes.func.isRequired
-};
+interface CompletionModalBodyProps {
+ block: string;
+ completedPercent: number;
+ superBlock: string;
+ t: (arg0: string, arg1?: { percent: number }) => string;
+}
-export class CompletionModalBody extends PureComponent {
- constructor(props) {
+interface CompletionModalBodyState {
+ // This type was driving me nuts - seems like `NodeJS.Timeout | null;` should work
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ progressInterval: any;
+ shownPercent: number;
+}
+
+export class CompletionModalBody extends PureComponent<
+ CompletionModalBodyProps,
+ CompletionModalBodyState
+> {
+ static displayName: string;
+ constructor(props: CompletionModalBodyProps) {
super(props);
this.state = {
@@ -23,7 +33,7 @@ export class CompletionModalBody extends PureComponent {
this.animateProgressBar = this.animateProgressBar.bind(this);
}
- animateProgressBar(completedPercent) {
+ animateProgressBar(completedPercent: number): void {
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
if (completedPercent > 100) completedPercent = 100;
@@ -54,11 +64,11 @@ export class CompletionModalBody extends PureComponent {
});
}
- componentWillUnmount() {
+ componentWillUnmount(): void {
clearInterval(this.state.progressInterval);
}
- render() {
+ render(): JSX.Element {
const { block, completedPercent, superBlock, t } = this.props;
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
@@ -84,7 +94,7 @@ export class CompletionModalBody extends PureComponent {
{t('learn.percent-complete', {
@@ -100,6 +110,5 @@ export class CompletionModalBody extends PureComponent {
}
CompletionModalBody.displayName = 'CompletionModalBody';
-CompletionModalBody.propTypes = propTypes;
export default withTranslation()(CompletionModalBody);
diff --git a/client/src/templates/Challenges/components/CompletionModal.test.js b/client/src/templates/Challenges/components/completion-modal.test.tsx
similarity index 95%
rename from client/src/templates/Challenges/components/CompletionModal.test.js
rename to client/src/templates/Challenges/components/completion-modal.test.tsx
index fd78bc09f6..8e6c76a760 100644
--- a/client/src/templates/Challenges/components/CompletionModal.test.js
+++ b/client/src/templates/Challenges/components/completion-modal.test.tsx
@@ -1,4 +1,4 @@
-import { getCompletedPercent } from './CompletionModal';
+import { getCompletedPercent } from './completion-modal';
jest.mock('../../../analytics');
diff --git a/client/src/templates/Challenges/components/CompletionModal.js b/client/src/templates/Challenges/components/completion-modal.tsx
similarity index 68%
rename from client/src/templates/Challenges/components/CompletionModal.js
rename to client/src/templates/Challenges/components/completion-modal.tsx
index 4862d89dd6..24d2842f17 100644
--- a/client/src/templates/Challenges/components/CompletionModal.js
+++ b/client/src/templates/Challenges/components/completion-modal.tsx
@@ -1,15 +1,18 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/restrict-template-expressions */
import React, { Component } from 'react';
-import PropTypes from 'prop-types';
import { noop } from 'lodash-es';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby';
import { withTranslation } from 'react-i18next';
+import { Dispatch } from 'redux';
import Login from '../../../components/Header/components/Login';
-import CompletionModalBody from './CompletionModalBody';
+import CompletionModalBody from './completion-modal-body';
import { dasherize } from '../../../../../utils/slugs';
+import { AllChallengeNodeType } from '../../../redux/prop-types';
import './completion-modal.css';
@@ -37,12 +40,12 @@ const mapStateToProps = createSelector(
isSignedInSelector,
successMessageSelector,
(
- files,
- { title, id },
- completedChallengesIds,
- isOpen,
- isSignedIn,
- message
+ files: Record,
+ { title, id }: { title: string; id: string },
+ completedChallengesIds: string[],
+ isOpen: boolean,
+ isSignedIn: boolean,
+ message: string
) => ({
files,
title,
@@ -54,13 +57,13 @@ const mapStateToProps = createSelector(
})
);
-const mapDispatchToProps = function (dispatch) {
+const mapDispatchToProps = function (dispatch: Dispatch) {
const dispatchers = {
close: () => dispatch(closeModal('completion')),
submitChallenge: () => {
dispatch(submitChallenge());
},
- allowBlockDonationRequests: block => {
+ allowBlockDonationRequests: (block: string) => {
dispatch(allowBlockDonationRequests(block));
},
executeGA
@@ -68,30 +71,11 @@ const mapDispatchToProps = function (dispatch) {
return () => dispatchers;
};
-const propTypes = {
- allowBlockDonationRequests: PropTypes.func,
- block: PropTypes.string,
- blockName: PropTypes.string,
- close: PropTypes.func.isRequired,
- completedChallengesIds: PropTypes.array,
- currentBlockIds: PropTypes.array,
- executeGA: PropTypes.func,
- files: PropTypes.object.isRequired,
- id: PropTypes.string,
- isOpen: PropTypes.bool,
- isSignedIn: PropTypes.bool.isRequired,
- message: PropTypes.string,
- submitChallenge: PropTypes.func.isRequired,
- superBlock: PropTypes.string,
- t: PropTypes.func.isRequired,
- title: PropTypes.string
-};
-
export function getCompletedPercent(
- completedChallengesIds = [],
- currentBlockIds = [],
- currentChallengeId
-) {
+ completedChallengesIds: string[] = [],
+ currentBlockIds: string[] = [],
+ currentChallengeId: string
+): number {
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
? completedChallengesIds
: [...completedChallengesIds, currentChallengeId];
@@ -107,36 +91,70 @@ export function getCompletedPercent(
return completedPercent > 100 ? 100 : completedPercent;
}
-export class CompletionModalInner extends Component {
- constructor(props) {
+interface CompletionModalsProps {
+ allowBlockDonationRequests: (arg0: string) => void;
+ block: string;
+ blockName: string;
+ close: () => void;
+ completedChallengesIds: string[];
+ currentBlockIds?: string[];
+ executeGA: () => void;
+ files: Record;
+ id: string;
+ isOpen: boolean;
+ isSignedIn: boolean;
+ message: string;
+ submitChallenge: () => void;
+ superBlock: string;
+ t: (arg0: string) => string;
+ title: string;
+}
+
+interface CompletionModalInnerState {
+ downloadURL: null | string;
+ completedPercent: number;
+}
+
+export class CompletionModalInner extends Component<
+ CompletionModalsProps,
+ CompletionModalInnerState
+> {
+ constructor(props: CompletionModalsProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleKeypress = this.handleKeypress.bind(this);
+
+ this.state = {
+ downloadURL: null,
+ completedPercent: 0
+ };
}
- state = {
- downloadURL: null,
- completedPercent: 0
- };
-
- static getDerivedStateFromProps(props, state) {
+ static getDerivedStateFromProps(
+ props: CompletionModalsProps,
+ state: CompletionModalInnerState
+ ): CompletionModalInnerState {
const { files, isOpen } = props;
if (!isOpen) {
- return null;
+ return { downloadURL: null, completedPercent: 0 };
}
const { downloadURL } = state;
if (downloadURL) {
URL.revokeObjectURL(downloadURL);
}
let newURL = null;
- if (Object.keys(files).length) {
- const filesForDownload = Object.keys(files)
+ const fileKeys = Object.keys(files);
+ if (fileKeys.length) {
+ const filesForDownload = fileKeys
.map(key => files[key])
- .reduce((allFiles, { path, contents }) => {
- const beforeText = `** start of ${path} **\n\n`;
- const afterText = `\n\n** end of ${path} **\n\n`;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ .reduce((allFiles, currentFile: any) => {
+ const beforeText = `** start of ${currentFile.path} **\n\n`;
+ const afterText = `\n\n** end of ${currentFile.path} **\n\n`;
allFiles +=
- files.length > 1 ? beforeText + contents + afterText : contents;
+ fileKeys.length > 1
+ ? `${beforeText}${currentFile.contents}${afterText}`
+ : currentFile.contents;
return allFiles;
}, '');
const blob = new Blob([filesForDownload], {
@@ -146,13 +164,13 @@ export class CompletionModalInner extends Component {
}
const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
- let completedPercent = isSignedIn
+ const completedPercent = isSignedIn
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
: 0;
return { downloadURL: newURL, completedPercent: completedPercent };
}
- handleKeypress(e) {
+ handleKeypress(e: React.KeyboardEvent): void {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
// Since Hotkeys also listens to Ctrl + Enter we have to stop this event
@@ -162,13 +180,13 @@ export class CompletionModalInner extends Component {
}
}
- handleSubmit() {
+ handleSubmit(): void {
this.props.submitChallenge();
this.checkBlockCompletion();
}
// check block completion for donation
- checkBlockCompletion() {
+ checkBlockCompletion(): void {
if (
this.state.completedPercent === 100 &&
!this.props.completedChallengesIds.includes(this.props.id)
@@ -177,14 +195,14 @@ export class CompletionModalInner extends Component {
}
}
- componentWillUnmount() {
+ componentWillUnmount(): void {
if (this.state.downloadURL) {
URL.revokeObjectURL(this.state.downloadURL);
}
this.props.close();
}
- render() {
+ render(): JSX.Element {
const {
block,
close,
@@ -212,6 +230,7 @@ export class CompletionModalInner extends Component {
dialogClassName='challenge-success-modal'
keyboard={true}
onHide={close}
+ // eslint-disable-next-line @typescript-eslint/unbound-method
onKeyDown={isOpen ? this.handleKeypress : noop}
show={isOpen}
>
@@ -236,7 +255,7 @@ export class CompletionModalInner extends Component {
block={true}
bsSize='large'
bsStyle='primary'
- onClick={this.handleSubmit}
+ onClick={() => this.handleSubmit()}
>
{isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')}
(Ctrl + Enter)
@@ -259,12 +278,10 @@ export class CompletionModalInner extends Component {
}
}
-CompletionModalInner.propTypes = propTypes;
-
-const useCurrentBlockIds = blockName => {
+const useCurrentBlockIds = (blockName: string) => {
const {
allChallengeNode: { edges }
- } = useStaticQuery(graphql`
+ }: { allChallengeNode: AllChallengeNodeType } = useStaticQuery(graphql`
query getCurrentBlockNodes {
allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) {
edges {
@@ -281,17 +298,18 @@ const useCurrentBlockIds = blockName => {
const currentBlockIds = edges
.filter(edge => edge.node.fields.blockName === blockName)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
.map(edge => edge.node.id);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return currentBlockIds;
};
-const CompletionModal = props => {
+const CompletionModal = (props: CompletionModalsProps) => {
const currentBlockIds = useCurrentBlockIds(props.blockName || '');
return ;
};
CompletionModal.displayName = 'CompletionModal';
-CompletionModal.propTypes = propTypes;
export default connect(
mapStateToProps,
diff --git a/client/src/templates/Challenges/projects/backend/Show.tsx b/client/src/templates/Challenges/projects/backend/Show.tsx
index 8f6f02b55d..13e301f74e 100644
--- a/client/src/templates/Challenges/projects/backend/Show.tsx
+++ b/client/src/templates/Challenges/projects/backend/Show.tsx
@@ -27,7 +27,7 @@ import ChallengeTitle from '../../components/challenge-title';
import ChallengeDescription from '../../components/Challenge-Description';
import TestSuite from '../../components/Test-Suite';
import Output from '../../components/Output';
-import CompletionModal from '../../components/CompletionModal';
+import CompletionModal from '../../components/completion-modal';
import HelpModal from '../../components/HelpModal';
import ProjectToolPanel from '../Tool-Panel';
import SolutionForm from '../SolutionForm';
diff --git a/client/src/templates/Challenges/projects/frontend/Show.tsx b/client/src/templates/Challenges/projects/frontend/Show.tsx
index b374b34929..728495955c 100644
--- a/client/src/templates/Challenges/projects/frontend/Show.tsx
+++ b/client/src/templates/Challenges/projects/frontend/Show.tsx
@@ -30,7 +30,7 @@ import ChallengeDescription from '../../components/Challenge-Description';
import Spacer from '../../../../components/helpers/spacer';
import SolutionForm from '../SolutionForm';
import ProjectToolPanel from '../Tool-Panel';
-import CompletionModal from '../../components/CompletionModal';
+import CompletionModal from '../../components/completion-modal';
import HelpModal from '../../components/HelpModal';
import Hotkeys from '../../components/Hotkeys';
diff --git a/client/src/templates/Challenges/video/Show.tsx b/client/src/templates/Challenges/video/Show.tsx
index 8fdc9a4ebe..1000a64984 100644
--- a/client/src/templates/Challenges/video/Show.tsx
+++ b/client/src/templates/Challenges/video/Show.tsx
@@ -21,7 +21,7 @@ import LearnLayout from '../../../components/layouts/Learn';
import ChallengeTitle from '../components/challenge-title';
import ChallengeDescription from '../components/Challenge-Description';
import Spacer from '../../../components/helpers/spacer';
-import CompletionModal from '../components/CompletionModal';
+import CompletionModal from '../components/completion-modal';
import Hotkeys from '../components/Hotkeys';
import Loader from '../../../components/helpers/loader';
import {