feat(client): ts-migrate challenge-templates complete-modal (#42598)
This commit is contained in:
@ -167,6 +167,7 @@ export type ChallengeNodeType = {
|
||||
guideUrl: string;
|
||||
head: string[];
|
||||
helpCategory: string;
|
||||
id: string;
|
||||
instructions: string;
|
||||
isComingSoon: boolean;
|
||||
removeComments: boolean;
|
||||
|
@ -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';
|
||||
|
@ -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();
|
@ -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);
|
@ -1,4 +1,4 @@
|
||||
import { getCompletedPercent } from './CompletionModal';
|
||||
import { getCompletedPercent } from './completion-modal';
|
||||
|
||||
jest.mock('../../../analytics');
|
||||
|
@ -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,
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user