feat(client): ts-migrate challenge-templates complete-modal (#42598)

This commit is contained in:
Tom
2021-06-25 10:22:50 -05:00
committed by Mrugesh Mohapatra
parent da461bf09a
commit 6cd8a025a7
10 changed files with 110 additions and 82 deletions

View File

@ -167,6 +167,7 @@ export type ChallengeNodeType = {
guideUrl: string;
head: string[];
helpCategory: string;
id: string;
instructions: string;
isComingSoon: boolean;
removeComments: boolean;

View File

@ -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';

View File

@ -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('<CompletionModalBody />', () => {
const { container } = render(<CompletionModalBody {...props} />);
fireEvent.animationEnd(
container.querySelector('.completion-success-icon')
container.querySelector('.completion-success-icon') as HTMLElement
);
jest.runAllTimers();

View File

@ -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 {
</div>
<div
className='progress-bar-percent'
style={{ width: this.state.shownPercent + '%' }}
style={{ width: `${this.state.shownPercent}%` }}
>
<div className='progress-bar-foreground'>
{t('learn.percent-complete', {
@ -100,6 +110,5 @@ export class CompletionModalBody extends PureComponent {
}
CompletionModalBody.displayName = 'CompletionModalBody';
CompletionModalBody.propTypes = propTypes;
export default withTranslation()(CompletionModalBody);

View File

@ -1,4 +1,4 @@
import { getCompletedPercent } from './CompletionModal';
import { getCompletedPercent } from './completion-modal';
jest.mock('../../../analytics');

View File

@ -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<string, unknown>,
{ 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<string, unknown>;
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<string>((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')}
<span className='hidden-xs'> (Ctrl + Enter)</span>
@ -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 <CompletionModalInner currentBlockIds={currentBlockIds} {...props} />;
};
CompletionModal.displayName = 'CompletionModal';
CompletionModal.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -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';

View File

@ -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';

View File

@ -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 {