mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-01-07 00:03:44 -05:00
feat(client): add "completed x of y" in completion modal (#47022)
* feat: wrote a new function and updated the test case * fix: lint errors * fix: Fixed the changes not being reflected in the code. And undid unwanted commits from the last time * fix: removing unwanted updates to the package-lock.json * fix: Removing unwanted changes from package-lock.json * fix: removing unwanted changes from package-lock.json * fix: updating snapshots to fix the test case * chore: update snapshot * Update client/src/templates/Challenges/components/completion-modal.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * Update client/src/templates/Challenges/components/completion-modal.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * Update client/src/templates/Challenges/components/completion-modal.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * fix: removed hard coded english words from translation text * fix: only show output when id is cert project * fix: correctly count completed challenges * fix: update test and snapshot [skip ci] * fix: at least one challenge is always completed When the modal pops-up, there is always at least one challenge counted as "complete" * refactor: slightly improve readability Co-authored-by: Ilenia <nethleen@gmail.com> Co-authored-by: Mrugesh Mohapatra <hi@mrugesh.dev> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -284,6 +284,7 @@
|
||||
"sign-in-save": "Sign in to save your progress",
|
||||
"download-solution": "Download my solution",
|
||||
"percent-complete": "{{percent}}% complete",
|
||||
"project-complete": "Completed {{completedChallengesInBlock}} of {{totalChallengesInBlock}} certification projects",
|
||||
"tried-rsa": "If you've already tried the <0>Read-Search-Ask</0> method, then you can ask for help on the freeCodeCamp forum.",
|
||||
"rsa": "Read, search, ask",
|
||||
"rsa-forum": "<strong>Before making a new post</strong> please see if your question has <0>already been answered on the forum</0>.",
|
||||
|
||||
@@ -8,6 +8,9 @@ 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
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ 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 {
|
||||
@@ -70,9 +74,23 @@ export class CompletionModalBody extends PureComponent<
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { block, completedPercent, superBlock, t } = this.props;
|
||||
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'>
|
||||
@@ -112,6 +130,14 @@ export class CompletionModalBody extends PureComponent<
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isCertificationProject && (
|
||||
<output>
|
||||
{t('learn.project-complete', {
|
||||
completedChallengesInBlock,
|
||||
totalChallengesInBlock
|
||||
})}
|
||||
</output>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,15 +5,10 @@ jest.mock('../../../analytics');
|
||||
const completedChallengesIds = ['1', '3', '5'],
|
||||
currentBlockIds = ['1', '3', '5', '7'],
|
||||
id = '7',
|
||||
fakeId = '12345',
|
||||
fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
|
||||
|
||||
describe('<CompletionModal />', () => {
|
||||
describe('getCompletedPercent', () => {
|
||||
it('returns 0 if no challenges have been completed', () => {
|
||||
expect(getCompletedPercent([], currentBlockIds, fakeId)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 25 if one out of four challenges are complete', () => {
|
||||
expect(getCompletedPercent([], currentBlockIds, currentBlockIds[1])).toBe(
|
||||
25
|
||||
|
||||
@@ -77,21 +77,33 @@ export function getCompletedPercent(
|
||||
currentBlockIds: string[] = [],
|
||||
currentChallengeId: string
|
||||
): number {
|
||||
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
|
||||
? completedChallengesIds
|
||||
: [...completedChallengesIds, currentChallengeId];
|
||||
|
||||
const completedChallengesInBlock = completedChallengesIds.filter(id => {
|
||||
return currentBlockIds.includes(id);
|
||||
});
|
||||
|
||||
const completedChallengesInBlock = getCompletedChallengesInBlock(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
currentChallengeId
|
||||
);
|
||||
const completedPercent = Math.round(
|
||||
(completedChallengesInBlock.length / currentBlockIds.length) * 100
|
||||
(completedChallengesInBlock / currentBlockIds.length) * 100
|
||||
);
|
||||
|
||||
return completedPercent > 100 ? 100 : completedPercent;
|
||||
}
|
||||
|
||||
function getCompletedChallengesInBlock(
|
||||
completedChallengesIds: string[],
|
||||
currentBlockChallengeIds: string[],
|
||||
currentChallengeId: string
|
||||
) {
|
||||
const oldCompletionCount = completedChallengesIds.filter(challengeId =>
|
||||
currentBlockChallengeIds.includes(challengeId)
|
||||
).length;
|
||||
|
||||
const isAlreadyCompleted =
|
||||
completedChallengesIds.includes(currentChallengeId);
|
||||
|
||||
return isAlreadyCompleted ? oldCompletionCount : oldCompletionCount + 1;
|
||||
}
|
||||
|
||||
interface CompletionModalsProps {
|
||||
allowBlockDonationRequests: (arg0: string) => void;
|
||||
block: string;
|
||||
@@ -116,6 +128,7 @@ interface CompletionModalsProps {
|
||||
interface CompletionModalInnerState {
|
||||
downloadURL: null | string;
|
||||
completedPercent: number;
|
||||
completedChallengesInBlock: number;
|
||||
}
|
||||
|
||||
export class CompletionModalInner extends Component<
|
||||
@@ -129,7 +142,8 @@ export class CompletionModalInner extends Component<
|
||||
|
||||
this.state = {
|
||||
downloadURL: null,
|
||||
completedPercent: 0
|
||||
completedPercent: 0,
|
||||
completedChallengesInBlock: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,7 +153,11 @@ export class CompletionModalInner extends Component<
|
||||
): CompletionModalInnerState {
|
||||
const { challengeFiles, isOpen } = props;
|
||||
if (!isOpen) {
|
||||
return { downloadURL: null, completedPercent: 0 };
|
||||
return {
|
||||
downloadURL: null,
|
||||
completedPercent: 0,
|
||||
completedChallengesInBlock: 0
|
||||
};
|
||||
}
|
||||
const { downloadURL } = state;
|
||||
if (downloadURL) {
|
||||
@@ -168,7 +186,21 @@ export class CompletionModalInner extends Component<
|
||||
const completedPercent = isSignedIn
|
||||
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||
: 0;
|
||||
return { downloadURL: newURL, completedPercent: completedPercent };
|
||||
|
||||
let completedChallengesInBlock = 0;
|
||||
if (currentBlockIds) {
|
||||
completedChallengesInBlock = getCompletedChallengesInBlock(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
downloadURL: newURL,
|
||||
completedPercent,
|
||||
completedChallengesInBlock
|
||||
};
|
||||
}
|
||||
|
||||
handleKeypress(e: React.KeyboardEvent): void {
|
||||
@@ -207,15 +239,19 @@ export class CompletionModalInner extends Component<
|
||||
const {
|
||||
block,
|
||||
close,
|
||||
currentBlockIds,
|
||||
id,
|
||||
isOpen,
|
||||
message,
|
||||
t,
|
||||
title,
|
||||
isSignedIn,
|
||||
superBlock = ''
|
||||
message,
|
||||
superBlock = '',
|
||||
t,
|
||||
title
|
||||
} = this.props;
|
||||
|
||||
const { completedPercent } = this.state;
|
||||
const { completedPercent, completedChallengesInBlock } = this.state;
|
||||
|
||||
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
|
||||
|
||||
if (isOpen) {
|
||||
executeGA({ type: 'modal', data: '/completion-modal' });
|
||||
@@ -243,9 +279,14 @@ export class CompletionModalInner extends Component<
|
||||
</Modal.Header>
|
||||
<Modal.Body className='completion-modal-body'>
|
||||
<CompletionModalBody
|
||||
block={block}
|
||||
completedPercent={completedPercent}
|
||||
superBlock={superBlock}
|
||||
{...{
|
||||
block,
|
||||
completedPercent,
|
||||
completedChallengesInBlock,
|
||||
currentChallengeId: id,
|
||||
superBlock,
|
||||
totalChallengesInBlock
|
||||
}}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
|
||||
Reference in New Issue
Block a user