feat(client): ts-migrate challenge-templates complete-modal (#42598)
This commit is contained in:
@ -167,6 +167,7 @@ export type ChallengeNodeType = {
|
|||||||
guideUrl: string;
|
guideUrl: string;
|
||||||
head: string[];
|
head: string[];
|
||||||
helpCategory: string;
|
helpCategory: string;
|
||||||
|
id: string;
|
||||||
instructions: string;
|
instructions: string;
|
||||||
isComingSoon: boolean;
|
isComingSoon: boolean;
|
||||||
removeComments: boolean;
|
removeComments: boolean;
|
||||||
|
@ -17,7 +17,7 @@ import MultifileEditor from './MultifileEditor';
|
|||||||
import Preview from '../components/Preview';
|
import Preview from '../components/Preview';
|
||||||
import SidePanel from '../components/Side-Panel';
|
import SidePanel from '../components/Side-Panel';
|
||||||
import Output from '../components/Output';
|
import Output from '../components/Output';
|
||||||
import CompletionModal from '../components/CompletionModal';
|
import CompletionModal from '../components/completion-modal';
|
||||||
import HelpModal from '../components/HelpModal';
|
import HelpModal from '../components/HelpModal';
|
||||||
import VideoModal from '../components/VideoModal';
|
import VideoModal from '../components/VideoModal';
|
||||||
import ResetModal from '../components/ResetModal';
|
import ResetModal from '../components/ResetModal';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
import { render, fireEvent } from '@testing-library/react';
|
||||||
|
|
||||||
import CompletionModalBody from './CompletionModalBody';
|
import CompletionModalBody from './completion-modal-body';
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
block: 'basic-html-and-html5',
|
block: 'basic-html-and-html5',
|
||||||
@ -33,7 +33,7 @@ describe('<CompletionModalBody />', () => {
|
|||||||
const { container } = render(<CompletionModalBody {...props} />);
|
const { container } = render(<CompletionModalBody {...props} />);
|
||||||
|
|
||||||
fireEvent.animationEnd(
|
fireEvent.animationEnd(
|
||||||
container.querySelector('.completion-success-icon')
|
container.querySelector('.completion-success-icon') as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
@ -1,18 +1,28 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import BezierEasing from 'bezier-easing';
|
import BezierEasing from 'bezier-easing';
|
||||||
import GreenPass from '../../../assets/icons/green-pass';
|
import GreenPass from '../../../assets/icons/green-pass';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const propTypes = {
|
interface CompletionModalBodyProps {
|
||||||
block: PropTypes.string,
|
block: string;
|
||||||
completedPercent: PropTypes.number,
|
completedPercent: number;
|
||||||
superBlock: PropTypes.string,
|
superBlock: string;
|
||||||
t: PropTypes.func.isRequired
|
t: (arg0: string, arg1?: { percent: number }) => string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export class CompletionModalBody extends PureComponent {
|
interface CompletionModalBodyState {
|
||||||
constructor(props) {
|
// 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);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -23,7 +33,7 @@ export class CompletionModalBody extends PureComponent {
|
|||||||
this.animateProgressBar = this.animateProgressBar.bind(this);
|
this.animateProgressBar = this.animateProgressBar.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
animateProgressBar(completedPercent) {
|
animateProgressBar(completedPercent: number): void {
|
||||||
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
|
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
|
||||||
|
|
||||||
if (completedPercent > 100) completedPercent = 100;
|
if (completedPercent > 100) completedPercent = 100;
|
||||||
@ -54,11 +64,11 @@ export class CompletionModalBody extends PureComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount(): void {
|
||||||
clearInterval(this.state.progressInterval);
|
clearInterval(this.state.progressInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const { block, completedPercent, superBlock, t } = this.props;
|
const { block, completedPercent, superBlock, t } = this.props;
|
||||||
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
||||||
|
|
||||||
@ -84,7 +94,7 @@ export class CompletionModalBody extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='progress-bar-percent'
|
className='progress-bar-percent'
|
||||||
style={{ width: this.state.shownPercent + '%' }}
|
style={{ width: `${this.state.shownPercent}%` }}
|
||||||
>
|
>
|
||||||
<div className='progress-bar-foreground'>
|
<div className='progress-bar-foreground'>
|
||||||
{t('learn.percent-complete', {
|
{t('learn.percent-complete', {
|
||||||
@ -100,6 +110,5 @@ export class CompletionModalBody extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompletionModalBody.displayName = 'CompletionModalBody';
|
CompletionModalBody.displayName = 'CompletionModalBody';
|
||||||
CompletionModalBody.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default withTranslation()(CompletionModalBody);
|
export default withTranslation()(CompletionModalBody);
|
@ -1,4 +1,4 @@
|
|||||||
import { getCompletedPercent } from './CompletionModal';
|
import { getCompletedPercent } from './completion-modal';
|
||||||
|
|
||||||
jest.mock('../../../analytics');
|
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 React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { noop } from 'lodash-es';
|
import { noop } from 'lodash-es';
|
||||||
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 { useStaticQuery, graphql } from 'gatsby';
|
import { useStaticQuery, graphql } from 'gatsby';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
|
||||||
import Login from '../../../components/Header/components/Login';
|
import Login from '../../../components/Header/components/Login';
|
||||||
import CompletionModalBody from './CompletionModalBody';
|
import CompletionModalBody from './completion-modal-body';
|
||||||
import { dasherize } from '../../../../../utils/slugs';
|
import { dasherize } from '../../../../../utils/slugs';
|
||||||
|
import { AllChallengeNodeType } from '../../../redux/prop-types';
|
||||||
|
|
||||||
import './completion-modal.css';
|
import './completion-modal.css';
|
||||||
|
|
||||||
@ -37,12 +40,12 @@ const mapStateToProps = createSelector(
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
successMessageSelector,
|
successMessageSelector,
|
||||||
(
|
(
|
||||||
files,
|
files: Record<string, unknown>,
|
||||||
{ title, id },
|
{ title, id }: { title: string; id: string },
|
||||||
completedChallengesIds,
|
completedChallengesIds: string[],
|
||||||
isOpen,
|
isOpen: boolean,
|
||||||
isSignedIn,
|
isSignedIn: boolean,
|
||||||
message
|
message: string
|
||||||
) => ({
|
) => ({
|
||||||
files,
|
files,
|
||||||
title,
|
title,
|
||||||
@ -54,13 +57,13 @@ const mapStateToProps = createSelector(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = function (dispatch) {
|
const mapDispatchToProps = function (dispatch: Dispatch) {
|
||||||
const dispatchers = {
|
const dispatchers = {
|
||||||
close: () => dispatch(closeModal('completion')),
|
close: () => dispatch(closeModal('completion')),
|
||||||
submitChallenge: () => {
|
submitChallenge: () => {
|
||||||
dispatch(submitChallenge());
|
dispatch(submitChallenge());
|
||||||
},
|
},
|
||||||
allowBlockDonationRequests: block => {
|
allowBlockDonationRequests: (block: string) => {
|
||||||
dispatch(allowBlockDonationRequests(block));
|
dispatch(allowBlockDonationRequests(block));
|
||||||
},
|
},
|
||||||
executeGA
|
executeGA
|
||||||
@ -68,30 +71,11 @@ const mapDispatchToProps = function (dispatch) {
|
|||||||
return () => dispatchers;
|
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(
|
export function getCompletedPercent(
|
||||||
completedChallengesIds = [],
|
completedChallengesIds: string[] = [],
|
||||||
currentBlockIds = [],
|
currentBlockIds: string[] = [],
|
||||||
currentChallengeId
|
currentChallengeId: string
|
||||||
) {
|
): number {
|
||||||
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
|
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
|
||||||
? completedChallengesIds
|
? completedChallengesIds
|
||||||
: [...completedChallengesIds, currentChallengeId];
|
: [...completedChallengesIds, currentChallengeId];
|
||||||
@ -107,36 +91,70 @@ export function getCompletedPercent(
|
|||||||
return completedPercent > 100 ? 100 : completedPercent;
|
return completedPercent > 100 ? 100 : completedPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CompletionModalInner extends Component {
|
interface CompletionModalsProps {
|
||||||
constructor(props) {
|
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);
|
super(props);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleKeypress = this.handleKeypress.bind(this);
|
this.handleKeypress = this.handleKeypress.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
downloadURL: null,
|
||||||
|
completedPercent: 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
static getDerivedStateFromProps(
|
||||||
downloadURL: null,
|
props: CompletionModalsProps,
|
||||||
completedPercent: 0
|
state: CompletionModalInnerState
|
||||||
};
|
): CompletionModalInnerState {
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
const { files, isOpen } = props;
|
const { files, isOpen } = props;
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return null;
|
return { downloadURL: null, completedPercent: 0 };
|
||||||
}
|
}
|
||||||
const { downloadURL } = state;
|
const { downloadURL } = state;
|
||||||
if (downloadURL) {
|
if (downloadURL) {
|
||||||
URL.revokeObjectURL(downloadURL);
|
URL.revokeObjectURL(downloadURL);
|
||||||
}
|
}
|
||||||
let newURL = null;
|
let newURL = null;
|
||||||
if (Object.keys(files).length) {
|
const fileKeys = Object.keys(files);
|
||||||
const filesForDownload = Object.keys(files)
|
if (fileKeys.length) {
|
||||||
|
const filesForDownload = fileKeys
|
||||||
.map(key => files[key])
|
.map(key => files[key])
|
||||||
.reduce((allFiles, { path, contents }) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const beforeText = `** start of ${path} **\n\n`;
|
.reduce<string>((allFiles, currentFile: any) => {
|
||||||
const afterText = `\n\n** end of ${path} **\n\n`;
|
const beforeText = `** start of ${currentFile.path} **\n\n`;
|
||||||
|
const afterText = `\n\n** end of ${currentFile.path} **\n\n`;
|
||||||
allFiles +=
|
allFiles +=
|
||||||
files.length > 1 ? beforeText + contents + afterText : contents;
|
fileKeys.length > 1
|
||||||
|
? `${beforeText}${currentFile.contents}${afterText}`
|
||||||
|
: currentFile.contents;
|
||||||
return allFiles;
|
return allFiles;
|
||||||
}, '');
|
}, '');
|
||||||
const blob = new Blob([filesForDownload], {
|
const blob = new Blob([filesForDownload], {
|
||||||
@ -146,13 +164,13 @@ export class CompletionModalInner extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
|
const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
|
||||||
let completedPercent = isSignedIn
|
const completedPercent = isSignedIn
|
||||||
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||||
: 0;
|
: 0;
|
||||||
return { downloadURL: newURL, completedPercent: completedPercent };
|
return { downloadURL: newURL, completedPercent: completedPercent };
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeypress(e) {
|
handleKeypress(e: React.KeyboardEvent): void {
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Since Hotkeys also listens to Ctrl + Enter we have to stop this event
|
// 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.props.submitChallenge();
|
||||||
this.checkBlockCompletion();
|
this.checkBlockCompletion();
|
||||||
}
|
}
|
||||||
|
|
||||||
// check block completion for donation
|
// check block completion for donation
|
||||||
checkBlockCompletion() {
|
checkBlockCompletion(): void {
|
||||||
if (
|
if (
|
||||||
this.state.completedPercent === 100 &&
|
this.state.completedPercent === 100 &&
|
||||||
!this.props.completedChallengesIds.includes(this.props.id)
|
!this.props.completedChallengesIds.includes(this.props.id)
|
||||||
@ -177,14 +195,14 @@ export class CompletionModalInner extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount(): void {
|
||||||
if (this.state.downloadURL) {
|
if (this.state.downloadURL) {
|
||||||
URL.revokeObjectURL(this.state.downloadURL);
|
URL.revokeObjectURL(this.state.downloadURL);
|
||||||
}
|
}
|
||||||
this.props.close();
|
this.props.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
block,
|
block,
|
||||||
close,
|
close,
|
||||||
@ -212,6 +230,7 @@ export class CompletionModalInner extends Component {
|
|||||||
dialogClassName='challenge-success-modal'
|
dialogClassName='challenge-success-modal'
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
onHide={close}
|
onHide={close}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
onKeyDown={isOpen ? this.handleKeypress : noop}
|
onKeyDown={isOpen ? this.handleKeypress : noop}
|
||||||
show={isOpen}
|
show={isOpen}
|
||||||
>
|
>
|
||||||
@ -236,7 +255,7 @@ export class CompletionModalInner extends Component {
|
|||||||
block={true}
|
block={true}
|
||||||
bsSize='large'
|
bsSize='large'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
onClick={this.handleSubmit}
|
onClick={() => this.handleSubmit()}
|
||||||
>
|
>
|
||||||
{isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')}
|
{isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')}
|
||||||
<span className='hidden-xs'> (Ctrl + Enter)</span>
|
<span className='hidden-xs'> (Ctrl + Enter)</span>
|
||||||
@ -259,12 +278,10 @@ export class CompletionModalInner extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CompletionModalInner.propTypes = propTypes;
|
const useCurrentBlockIds = (blockName: string) => {
|
||||||
|
|
||||||
const useCurrentBlockIds = blockName => {
|
|
||||||
const {
|
const {
|
||||||
allChallengeNode: { edges }
|
allChallengeNode: { edges }
|
||||||
} = useStaticQuery(graphql`
|
}: { allChallengeNode: AllChallengeNodeType } = useStaticQuery(graphql`
|
||||||
query getCurrentBlockNodes {
|
query getCurrentBlockNodes {
|
||||||
allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) {
|
allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) {
|
||||||
edges {
|
edges {
|
||||||
@ -281,17 +298,18 @@ const useCurrentBlockIds = blockName => {
|
|||||||
|
|
||||||
const currentBlockIds = edges
|
const currentBlockIds = edges
|
||||||
.filter(edge => edge.node.fields.blockName === blockName)
|
.filter(edge => edge.node.fields.blockName === blockName)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
.map(edge => edge.node.id);
|
.map(edge => edge.node.id);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return currentBlockIds;
|
return currentBlockIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CompletionModal = props => {
|
const CompletionModal = (props: CompletionModalsProps) => {
|
||||||
const currentBlockIds = useCurrentBlockIds(props.blockName || '');
|
const currentBlockIds = useCurrentBlockIds(props.blockName || '');
|
||||||
return <CompletionModalInner currentBlockIds={currentBlockIds} {...props} />;
|
return <CompletionModalInner currentBlockIds={currentBlockIds} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
CompletionModal.displayName = 'CompletionModal';
|
CompletionModal.displayName = 'CompletionModal';
|
||||||
CompletionModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
@ -27,7 +27,7 @@ import ChallengeTitle from '../../components/challenge-title';
|
|||||||
import ChallengeDescription from '../../components/Challenge-Description';
|
import ChallengeDescription from '../../components/Challenge-Description';
|
||||||
import TestSuite from '../../components/Test-Suite';
|
import TestSuite from '../../components/Test-Suite';
|
||||||
import Output from '../../components/Output';
|
import Output from '../../components/Output';
|
||||||
import CompletionModal from '../../components/CompletionModal';
|
import CompletionModal from '../../components/completion-modal';
|
||||||
import HelpModal from '../../components/HelpModal';
|
import HelpModal from '../../components/HelpModal';
|
||||||
import ProjectToolPanel from '../Tool-Panel';
|
import ProjectToolPanel from '../Tool-Panel';
|
||||||
import SolutionForm from '../SolutionForm';
|
import SolutionForm from '../SolutionForm';
|
||||||
|
@ -30,7 +30,7 @@ import ChallengeDescription from '../../components/Challenge-Description';
|
|||||||
import Spacer from '../../../../components/helpers/spacer';
|
import Spacer from '../../../../components/helpers/spacer';
|
||||||
import SolutionForm from '../SolutionForm';
|
import SolutionForm from '../SolutionForm';
|
||||||
import ProjectToolPanel from '../Tool-Panel';
|
import ProjectToolPanel from '../Tool-Panel';
|
||||||
import CompletionModal from '../../components/CompletionModal';
|
import CompletionModal from '../../components/completion-modal';
|
||||||
import HelpModal from '../../components/HelpModal';
|
import HelpModal from '../../components/HelpModal';
|
||||||
import Hotkeys from '../../components/Hotkeys';
|
import Hotkeys from '../../components/Hotkeys';
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ 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';
|
||||||
import Spacer from '../../../components/helpers/spacer';
|
import Spacer from '../../../components/helpers/spacer';
|
||||||
import CompletionModal from '../components/CompletionModal';
|
import CompletionModal from '../components/completion-modal';
|
||||||
import Hotkeys from '../components/Hotkeys';
|
import Hotkeys from '../components/Hotkeys';
|
||||||
import Loader from '../../../components/helpers/loader';
|
import Loader from '../../../components/helpers/loader';
|
||||||
import {
|
import {
|
||||||
|
Reference in New Issue
Block a user