mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
feat(client): isolate progress bar (#49479)
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
co-authored by
Shaun Hamilton
parent
0f3083e492
commit
8bd84760b3
@@ -0,0 +1 @@
|
||||
export { default } from './progress-bar';
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import BezierEasing from 'bezier-easing';
|
||||
|
||||
interface ProgressBarInnerProps {
|
||||
completedPercent: number;
|
||||
title: string;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
|
||||
const intervalLength = 10;
|
||||
let percent = 0;
|
||||
let applyAnimation = true;
|
||||
|
||||
function ProgressBarInner({
|
||||
completedPercent,
|
||||
title,
|
||||
meta
|
||||
}: ProgressBarInnerProps): JSX.Element {
|
||||
const [shownPercent, setShownPercent] = useState(0);
|
||||
const [progressInterval, setProgressInterval] = useState(0);
|
||||
const [progressBarInnerWidth, setProgressBarInnerWidth] = useState(0);
|
||||
const progressBarInnerWrap = useRef<HTMLDivElement>(null);
|
||||
|
||||
const animateProgressBarInner = (completedPercent: number) => {
|
||||
if (completedPercent > 100) completedPercent = 100;
|
||||
if (completedPercent < 0) completedPercent = 0;
|
||||
|
||||
const transitionLength = completedPercent * 10 + 750;
|
||||
const intervalsToFinish = transitionLength / intervalLength;
|
||||
const amountPerInterval = completedPercent / intervalsToFinish;
|
||||
|
||||
const myInterval = window.setInterval(() => {
|
||||
percent += amountPerInterval;
|
||||
|
||||
if (percent > completedPercent) percent = completedPercent;
|
||||
|
||||
setShownPercent(
|
||||
Math.round(completedPercent * easing(percent / completedPercent))
|
||||
);
|
||||
if (percent >= completedPercent) clearInterval(myInterval);
|
||||
}, intervalLength);
|
||||
|
||||
setProgressInterval(myInterval);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (applyAnimation) animateProgressBarInner(completedPercent);
|
||||
return () => {
|
||||
if (progressInterval !== null) {
|
||||
clearInterval(progressInterval);
|
||||
applyAnimation = false;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [completedPercent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (progressBarInnerWrap.current)
|
||||
setProgressBarInnerWidth(progressBarInnerWrap.current.offsetWidth);
|
||||
}, [progressBarInnerWrap]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='completion-block-name'>{title}</div>
|
||||
<div
|
||||
className='progress-bar-wrap'
|
||||
aria-hidden='true'
|
||||
ref={progressBarInnerWrap}
|
||||
>
|
||||
<div className='progress-bar-background'></div>
|
||||
<div
|
||||
className='progress-bar-percent'
|
||||
data-testid='fcc-progress-bar-percent'
|
||||
style={{ width: `${shownPercent}%` }}
|
||||
>
|
||||
<div
|
||||
className='progress-bar-foreground'
|
||||
style={{ width: progressBarInnerWidth }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='completion-block-meta'>{meta}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProgressBarInner.displayName = 'ProgressBarInner';
|
||||
|
||||
export default ProgressBarInner;
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import {
|
||||
challengeMetaSelector,
|
||||
currentBlockIdsSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
completedPercentageSelector
|
||||
} from '../../templates/Challenges/redux/selectors';
|
||||
import { certMap } from '../../resources/cert-and-project-map';
|
||||
import ProgressBarInner from './progress-bar-inner';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
currentBlockIdsSelector,
|
||||
challengeMetaSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
completedPercentageSelector,
|
||||
(
|
||||
currentBlockIds: string[],
|
||||
{
|
||||
id,
|
||||
block,
|
||||
superBlock
|
||||
}: {
|
||||
id: string;
|
||||
block: string;
|
||||
superBlock: string;
|
||||
},
|
||||
completedChallengesInBlock: number,
|
||||
completedPercent: number
|
||||
) => ({
|
||||
currentBlockIds,
|
||||
id,
|
||||
block,
|
||||
superBlock,
|
||||
completedChallengesInBlock,
|
||||
completedPercent
|
||||
})
|
||||
);
|
||||
|
||||
type StateProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface ProgressBarProps extends StateProps {
|
||||
t: TFunction;
|
||||
}
|
||||
function ProgressBar({
|
||||
currentBlockIds,
|
||||
block,
|
||||
id,
|
||||
superBlock,
|
||||
completedChallengesInBlock,
|
||||
completedPercent,
|
||||
t
|
||||
}: ProgressBarProps): JSX.Element {
|
||||
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
||||
const isCertificationProject = certMap.some(cert => {
|
||||
if ('projects' in cert) {
|
||||
return cert.projects.some((project: { id: string }) => project.id === id);
|
||||
}
|
||||
});
|
||||
|
||||
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
|
||||
const meta =
|
||||
isCertificationProject && totalChallengesInBlock > 0
|
||||
? t('learn.project-complete', {
|
||||
completedChallengesInBlock,
|
||||
totalChallengesInBlock
|
||||
})
|
||||
: t('learn.percent-complete', {
|
||||
percent: completedPercent
|
||||
});
|
||||
return (
|
||||
<div className='progress-bar-container'>
|
||||
<ProgressBarInner
|
||||
title={blockTitle}
|
||||
meta={meta}
|
||||
completedPercent={completedPercent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ProgressBar.displayName = 'ProgressBar';
|
||||
|
||||
export default connect(mapStateToProps)(withTranslation()(ProgressBar));
|
||||
@@ -142,10 +142,10 @@ textarea.inputarea {
|
||||
|
||||
.test-feedback .test-status,
|
||||
.test-feedback .hint-status {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-direction: row;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.test-status-description,
|
||||
@@ -186,3 +186,20 @@ textarea.inputarea {
|
||||
.lower-jaw-icon-bar > button:last-child {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.editor-lower-jaw .progress-bar-wrap-container {
|
||||
width: 100% !important;
|
||||
margin: 32px 0px;
|
||||
}
|
||||
|
||||
.editor-lower-jaw .progress-bar-background-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.hint-status {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
@@ -167,7 +167,6 @@ function ShowClassic({
|
||||
superBlock,
|
||||
helpCategory,
|
||||
forumTopicId,
|
||||
certification,
|
||||
usesMultifileEditor,
|
||||
notes,
|
||||
videoUrl,
|
||||
@@ -477,12 +476,7 @@ function ShowClassic({
|
||||
windowTitle={windowTitle}
|
||||
/>
|
||||
</Media>
|
||||
<CompletionModal
|
||||
block={block}
|
||||
blockName={blockName}
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
<VideoModal videoUrl={videoUrl} />
|
||||
<ResetModal />
|
||||
@@ -518,7 +512,6 @@ export const query = graphql`
|
||||
helpCategory
|
||||
videoUrl
|
||||
superBlock
|
||||
certification
|
||||
translationPending
|
||||
forumTopicId
|
||||
fields {
|
||||
|
||||
@@ -178,15 +178,12 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
block,
|
||||
certification,
|
||||
challengeType,
|
||||
description,
|
||||
fields: { blockName },
|
||||
id: challengeId,
|
||||
instructions,
|
||||
notes,
|
||||
superBlock,
|
||||
title,
|
||||
translationPending,
|
||||
url
|
||||
@@ -351,12 +348,7 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
|
||||
<br />
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
<CompletionModal
|
||||
block={block}
|
||||
blockName={blockName}
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -378,8 +370,6 @@ export const query = graphql`
|
||||
query CodeAllyChallenge($slug: String!) {
|
||||
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
|
||||
challenge {
|
||||
block
|
||||
certification
|
||||
challengeType
|
||||
description
|
||||
fields {
|
||||
@@ -389,7 +379,6 @@ export const query = graphql`
|
||||
id
|
||||
instructions
|
||||
notes
|
||||
superBlock
|
||||
title
|
||||
translationPending
|
||||
url
|
||||
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<CompletionModalBody /> matches snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="completion-challenge-details"
|
||||
>
|
||||
<svg
|
||||
aria-label="icons.passed"
|
||||
class="completion-success-icon"
|
||||
data-testid="fcc-completion-success-icon"
|
||||
height="15"
|
||||
viewBox="0 0 200 200"
|
||||
width="15"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
aria-hidden="true"
|
||||
>
|
||||
<title>
|
||||
icons.passed
|
||||
</title>
|
||||
<circle
|
||||
cx="100"
|
||||
cy="99"
|
||||
fill="var(--primary-color)"
|
||||
r="95"
|
||||
stroke="var(--primary-color)"
|
||||
stroke-dasharray="null"
|
||||
/>
|
||||
<rect
|
||||
fill="var(--primary-background)"
|
||||
height="30"
|
||||
stroke="var(--primary-background)"
|
||||
stroke-dasharray="null"
|
||||
transform="rotate(-45, 120, 106.321)"
|
||||
width="128.85878"
|
||||
x="55.57059"
|
||||
y="91.32089"
|
||||
/>
|
||||
<rect
|
||||
fill="var(--primary-background)"
|
||||
height="30"
|
||||
stroke="var(--primary-background)"
|
||||
stroke-dasharray="null"
|
||||
transform="rotate(45, 66.75, 123.75)"
|
||||
width="80.66548"
|
||||
x="26.41726"
|
||||
y="108.75"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="completion-block-details"
|
||||
>
|
||||
<div
|
||||
class="completion-block-name"
|
||||
>
|
||||
intro:responsive-web-design.blocks.basic-html-and-html5.title
|
||||
</div>
|
||||
<div
|
||||
aria-label="learn.percent-complete"
|
||||
class="progress-bar-wrap"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="progress-bar-background"
|
||||
>
|
||||
learn.percent-complete
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="progress-bar-percent"
|
||||
data-testid="fcc-progress-bar-percent"
|
||||
style="width: 0%;"
|
||||
>
|
||||
<div
|
||||
class="progress-bar-foreground"
|
||||
>
|
||||
learn.percent-complete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { SuperBlocks } from '../../../../../config/certification-settings';
|
||||
|
||||
import CompletionModalBody from './completion-modal-body';
|
||||
|
||||
const props = {
|
||||
block: 'basic-html-and-html5',
|
||||
completedPercent: Math.floor(Math.random() * 101),
|
||||
completedChallengesInBlock: 2,
|
||||
totalChallengesInBlock: 5,
|
||||
currentChallengeId: '',
|
||||
superBlock: SuperBlocks.RespWebDesign
|
||||
};
|
||||
|
||||
describe('<CompletionModalBody />', () => {
|
||||
test('matches snapshot', () => {
|
||||
const { container } = render(<CompletionModalBody {...props} />);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('progress-bar', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
test('renders with 0% width initially', () => {
|
||||
render(<CompletionModalBody {...props} />);
|
||||
expect(screen.getByTestId('fcc-progress-bar-percent')).toHaveStyle({
|
||||
width: '0%'
|
||||
});
|
||||
});
|
||||
|
||||
test('has the correct width after animation', () => {
|
||||
render(<CompletionModalBody {...props} />);
|
||||
|
||||
fireEvent.animationEnd(screen.getByTestId('fcc-completion-success-icon'));
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(screen.getByTestId('fcc-progress-bar-percent')).toHaveStyle({
|
||||
width: `${props.completedPercent}%`
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
import BezierEasing from 'bezier-easing';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { certMap } from '../../../resources/cert-and-project-map';
|
||||
|
||||
interface CompletionModalBodyProps {
|
||||
block: string;
|
||||
completedChallengesInBlock: number;
|
||||
completedPercent: number;
|
||||
currentChallengeId: string;
|
||||
superBlock: string;
|
||||
t: TFunction;
|
||||
totalChallengesInBlock: number;
|
||||
}
|
||||
|
||||
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: number | null;
|
||||
shownPercent: number;
|
||||
}
|
||||
|
||||
export class CompletionModalBody extends PureComponent<
|
||||
CompletionModalBodyProps,
|
||||
CompletionModalBodyState
|
||||
> {
|
||||
static displayName: string;
|
||||
constructor(props: CompletionModalBodyProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
progressInterval: null,
|
||||
shownPercent: 0
|
||||
};
|
||||
|
||||
this.animateProgressBar = this.animateProgressBar.bind(this);
|
||||
}
|
||||
|
||||
animateProgressBar(completedPercent: number): void {
|
||||
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
|
||||
|
||||
if (completedPercent > 100) completedPercent = 100;
|
||||
if (completedPercent < 0) completedPercent = 0;
|
||||
|
||||
const transitionLength = completedPercent * 10 + 750;
|
||||
const intervalLength = 10;
|
||||
const intervalsToFinish = transitionLength / intervalLength;
|
||||
const amountPerInterval = completedPercent / intervalsToFinish;
|
||||
let percent = 0;
|
||||
|
||||
const myInterval = window.setInterval(() => {
|
||||
percent += amountPerInterval;
|
||||
|
||||
if (percent > completedPercent) percent = completedPercent;
|
||||
|
||||
this.setState({
|
||||
shownPercent: Math.round(
|
||||
completedPercent * easing(percent / completedPercent)
|
||||
)
|
||||
});
|
||||
|
||||
if (percent >= completedPercent) clearInterval(myInterval);
|
||||
}, intervalLength);
|
||||
|
||||
this.setState({
|
||||
progressInterval: myInterval
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.state.progressInterval !== null)
|
||||
clearInterval(this.state.progressInterval);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {
|
||||
block,
|
||||
completedPercent,
|
||||
totalChallengesInBlock,
|
||||
completedChallengesInBlock,
|
||||
currentChallengeId,
|
||||
superBlock,
|
||||
t
|
||||
} = this.props;
|
||||
const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`);
|
||||
const isCertificationProject = certMap.some(cert => {
|
||||
// @ts-expect-error If `projects` does not exist, no consequences
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
return cert.projects?.some(
|
||||
(project: { id: string }) => project.id === currentChallengeId
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='completion-challenge-details'>
|
||||
<GreenPass
|
||||
className='completion-success-icon'
|
||||
data-testid='fcc-completion-success-icon'
|
||||
onAnimationEnd={() => {
|
||||
setTimeout(() => {
|
||||
this.animateProgressBar(completedPercent);
|
||||
}, 50);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='completion-block-details'>
|
||||
<div className='completion-block-name'>{blockTitle}</div>
|
||||
<div
|
||||
className='progress-bar-wrap'
|
||||
aria-label={t('learn.percent-complete', {
|
||||
percent: completedPercent
|
||||
})}
|
||||
>
|
||||
<div className='progress-bar-background' aria-hidden='true'>
|
||||
{t('learn.percent-complete', {
|
||||
percent: this.state.shownPercent
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='progress-bar-percent'
|
||||
data-testid='fcc-progress-bar-percent'
|
||||
style={{ width: `${this.state.shownPercent}%` }}
|
||||
>
|
||||
<div className='progress-bar-foreground'>
|
||||
{t('learn.percent-complete', {
|
||||
percent: this.state.shownPercent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isCertificationProject && totalChallengesInBlock > 0 && (
|
||||
<output>
|
||||
{t('learn.project-complete', {
|
||||
completedChallengesInBlock,
|
||||
totalChallengesInBlock
|
||||
})}
|
||||
</output>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CompletionModalBody.displayName = 'CompletionModalBody';
|
||||
|
||||
export default withTranslation()(CompletionModalBody);
|
||||
@@ -51,27 +51,42 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.completion-block-name {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.completion-block-meta {
|
||||
color: var(--quaternary-color);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.completion-block-details .completion-block-name {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.completion-block-details .completion-block-meta {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.progress-bar-wrap {
|
||||
width: 400px;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar-background {
|
||||
width: 400px;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
color: var(--primary-color);
|
||||
border: 3px solid var(--primary-color);
|
||||
background-color: var(--quaternary-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -92,7 +107,7 @@
|
||||
.progress-bar-foreground {
|
||||
color: var(--primary-background);
|
||||
width: 400px;
|
||||
height: 50px;
|
||||
height: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -113,8 +128,7 @@
|
||||
.progress-bar-wrap,
|
||||
.progress-bar-background,
|
||||
.progress-bar-foreground {
|
||||
width: 260px;
|
||||
height: 40px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.completion-success-icon {
|
||||
@@ -129,7 +143,6 @@
|
||||
|
||||
.completion-challenge-name,
|
||||
.completion-block-name {
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { noop } from 'lodash-es';
|
||||
import React, { Component } from 'react';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { dasherize } from '../../../../../utils/slugs';
|
||||
@@ -22,12 +21,10 @@ import {
|
||||
isCompletionModalOpenSelector,
|
||||
successMessageSelector,
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector,
|
||||
completedPercentageSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
currentBlockIdsSelector
|
||||
challengeMetaSelector
|
||||
} from '../redux/selectors';
|
||||
import CompletionModalBody from './completion-modal-body';
|
||||
import ProgressBar from '../../../components/ProgressBar';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
|
||||
import './completion-modal.css';
|
||||
|
||||
@@ -39,72 +36,39 @@ const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
allChallengesInfoSelector,
|
||||
successMessageSelector,
|
||||
completedPercentageSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
currentBlockIdsSelector,
|
||||
|
||||
(
|
||||
challengeFiles: ChallengeFiles,
|
||||
{
|
||||
title,
|
||||
id,
|
||||
challengeType
|
||||
}: { title: string; id: string; challengeType: number },
|
||||
{ title }: { title: string; id: string },
|
||||
completedChallengesIds: string[],
|
||||
isOpen: boolean,
|
||||
isSignedIn: boolean,
|
||||
allChallengesInfo: AllChallengesInfo,
|
||||
message: string,
|
||||
completedPercent: number,
|
||||
completedChallengesInBlock: number,
|
||||
currentBlockIds: string[]
|
||||
message: string
|
||||
) => ({
|
||||
challengeFiles,
|
||||
title,
|
||||
id,
|
||||
challengeType,
|
||||
completedChallengesIds,
|
||||
isOpen,
|
||||
isSignedIn,
|
||||
allChallengesInfo,
|
||||
message,
|
||||
completedPercent,
|
||||
completedChallengesInBlock,
|
||||
currentBlockIds
|
||||
message
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = function (dispatch: Dispatch) {
|
||||
const dispatchers = {
|
||||
close: () => dispatch(closeModal('completion')),
|
||||
submitChallenge: () => {
|
||||
dispatch(submitChallenge());
|
||||
},
|
||||
executeGA
|
||||
};
|
||||
return () => dispatchers;
|
||||
const mapDispatchToProps = {
|
||||
close: () => closeModal('completion'),
|
||||
submitChallenge,
|
||||
executeGA
|
||||
};
|
||||
|
||||
interface CompletionModalsProps {
|
||||
block: string;
|
||||
blockName: string;
|
||||
certification: string;
|
||||
challengeType: number;
|
||||
type StateProps = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
interface CompletionModalsProps extends StateProps {
|
||||
close: () => void;
|
||||
completedChallengesIds: string[];
|
||||
executeGA: () => void;
|
||||
challengeFiles: ChallengeFiles;
|
||||
id: string;
|
||||
isOpen: boolean;
|
||||
isSignedIn: boolean;
|
||||
allChallengesInfo: AllChallengesInfo;
|
||||
message: string;
|
||||
completedPercent: number;
|
||||
completedChallengesInBlock: number;
|
||||
currentBlockIds: string[];
|
||||
submitChallenge: () => void;
|
||||
superBlock: string;
|
||||
t: TFunction;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface CompletionModalState {
|
||||
@@ -179,23 +143,8 @@ class CompletionModal extends Component<
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {
|
||||
block,
|
||||
close,
|
||||
id,
|
||||
isOpen,
|
||||
isSignedIn,
|
||||
message,
|
||||
superBlock = '',
|
||||
t,
|
||||
title,
|
||||
completedPercent,
|
||||
completedChallengesInBlock,
|
||||
currentBlockIds,
|
||||
submitChallenge
|
||||
} = this.props;
|
||||
|
||||
const totalChallengesInBlock = currentBlockIds.length;
|
||||
const { close, isOpen, isSignedIn, message, t, title, submitChallenge } =
|
||||
this.props;
|
||||
|
||||
if (isOpen) {
|
||||
executeGA({ event: 'pageview', pagePath: '/completion-modal' });
|
||||
@@ -222,16 +171,15 @@ class CompletionModal extends Component<
|
||||
<Modal.Title className='completion-message'>{message}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='completion-modal-body'>
|
||||
<CompletionModalBody
|
||||
{...{
|
||||
block,
|
||||
completedPercent,
|
||||
completedChallengesInBlock,
|
||||
currentChallengeId: id,
|
||||
superBlock,
|
||||
totalChallengesInBlock
|
||||
}}
|
||||
/>
|
||||
<div className='completion-challenge-details'>
|
||||
<GreenPass
|
||||
className='completion-success-icon'
|
||||
data-testid='fcc-completion-success-icon'
|
||||
/>
|
||||
</div>
|
||||
<div className='completion-block-details'>
|
||||
<ProgressBar />
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{isSignedIn ? null : (
|
||||
|
||||
@@ -202,11 +202,9 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { blockName },
|
||||
title,
|
||||
description,
|
||||
superBlock,
|
||||
certification,
|
||||
block,
|
||||
videoId,
|
||||
videoLocaleIds,
|
||||
@@ -360,12 +358,7 @@ class ShowOdin extends Component<ShowOdinProps, ShowOdinState> {
|
||||
</Button>
|
||||
<Spacer size='large' />
|
||||
</Col>
|
||||
<CompletionModal
|
||||
block={block}
|
||||
blockName={blockName}
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<CompletionModal />
|
||||
</Row>
|
||||
</Grid>
|
||||
</LearnLayout>
|
||||
@@ -401,10 +394,8 @@ export const query = graphql`
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
certification
|
||||
block
|
||||
fields {
|
||||
blockName
|
||||
slug
|
||||
}
|
||||
question {
|
||||
|
||||
@@ -199,7 +199,6 @@ class BackEnd extends Component<BackEndProps> {
|
||||
description,
|
||||
instructions,
|
||||
translationPending,
|
||||
certification,
|
||||
superBlock,
|
||||
block
|
||||
}
|
||||
@@ -265,12 +264,7 @@ class BackEnd extends Component<BackEndProps> {
|
||||
<TestSuite tests={tests} />
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
<CompletionModal
|
||||
block={block}
|
||||
blockName={blockName}
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -297,7 +291,6 @@ export const query = graphql`
|
||||
instructions
|
||||
challengeType
|
||||
helpCategory
|
||||
certification
|
||||
superBlock
|
||||
block
|
||||
translationPending
|
||||
|
||||
@@ -141,7 +141,6 @@ class Project extends Component<ProjectProps> {
|
||||
description,
|
||||
instructions,
|
||||
superBlock,
|
||||
certification,
|
||||
block,
|
||||
translationPending
|
||||
}
|
||||
@@ -196,12 +195,7 @@ class Project extends Component<ProjectProps> {
|
||||
<br />
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
<CompletionModal
|
||||
block={block}
|
||||
blockName={blockName}
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -229,7 +223,6 @@ export const query = graphql`
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
certification
|
||||
block
|
||||
translationPending
|
||||
fields {
|
||||
|
||||
@@ -176,11 +176,9 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { blockName },
|
||||
title,
|
||||
description,
|
||||
superBlock,
|
||||
certification,
|
||||
block,
|
||||
translationPending,
|
||||
videoId,
|
||||
@@ -298,12 +296,7 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
|
||||
</Button>
|
||||
<Spacer size='large' />
|
||||
</Col>
|
||||
<CompletionModal
|
||||
block={block}
|
||||
blockName={blockName}
|
||||
certification={certification}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
<CompletionModal />
|
||||
</Row>
|
||||
</Grid>
|
||||
</LearnLayout>
|
||||
@@ -339,10 +332,8 @@ export const query = graphql`
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
certification
|
||||
block
|
||||
fields {
|
||||
blockName
|
||||
slug
|
||||
}
|
||||
question {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
describe('progress bar', () => {
|
||||
beforeEach(() => {
|
||||
cy.exec('pnpm run seed');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it(
|
||||
'Should show the progress bar showing the completed percent',
|
||||
{ browser: 'electron' },
|
||||
() => {
|
||||
cy.visit(
|
||||
'/learn/javascript-algorithms-and-data-structures/basic-javascript/declare-javascript-variables'
|
||||
);
|
||||
cy.get(`${'.react-monaco-editor-container'} textarea`, { timeout: 16000 })
|
||||
.click()
|
||||
.focused()
|
||||
.type('{ctrl}a')
|
||||
.clear()
|
||||
.type('var myName;');
|
||||
cy.contains('Run the Tests (Ctrl + Enter)').click({ force: true });
|
||||
cy.contains('Submit and go to next challenge');
|
||||
cy.get('.progress-bar-container').contains('1% complete');
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user