mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-13 13:00:15 -04:00
feat(client): move the block donation modal logic to an epic. (#49381)
feat(client): move percent calculation logic to epic
This commit is contained in:
@@ -23,10 +23,12 @@ import { playTone } from '../../utils/tone';
|
||||
import { Spacer } from '../helpers';
|
||||
import DonateForm from './donate-form';
|
||||
|
||||
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isDonationModalOpenSelector,
|
||||
recentlyClaimedBlockSelector,
|
||||
(show: boolean, recentlyClaimedBlock: string) => ({
|
||||
(show: boolean, recentlyClaimedBlock: RecentlyClaimedBlock) => ({
|
||||
show,
|
||||
recentlyClaimedBlock
|
||||
})
|
||||
@@ -46,7 +48,7 @@ type DonateModalProps = {
|
||||
closeDonationModal: typeof closeDonationModal;
|
||||
executeGA: typeof executeGA;
|
||||
location?: WindowLocation;
|
||||
recentlyClaimedBlock: string;
|
||||
recentlyClaimedBlock: RecentlyClaimedBlock;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
@@ -70,13 +72,13 @@ function DonateModal({
|
||||
executeGA({
|
||||
event: 'donationview',
|
||||
action: `Displayed ${
|
||||
recentlyClaimedBlock ? 'Block' : 'Progress'
|
||||
recentlyClaimedBlock !== null ? 'Block' : 'Progress'
|
||||
} Donation Modal`
|
||||
});
|
||||
}
|
||||
}, [show, recentlyClaimedBlock, executeGA]);
|
||||
|
||||
const getDonationText = () => {
|
||||
const getCommonDonationText = () => {
|
||||
const donationDuration = modalDefaultDonation.donationDuration;
|
||||
switch (donationDuration) {
|
||||
case 'one-time':
|
||||
@@ -95,32 +97,28 @@ function DonateModal({
|
||||
}
|
||||
};
|
||||
|
||||
const blockDonationText = (
|
||||
const donationText = (
|
||||
<div className=' text-center block-modal-text'>
|
||||
<div className='donation-icon-container'>
|
||||
<Cup className='donation-icon' />
|
||||
</div>
|
||||
<Row>
|
||||
{!closeLabel && (
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<b>{t('donate.nicely-done', { block: recentlyClaimedBlock })}</b>
|
||||
<br />
|
||||
{getDonationText()}
|
||||
</Col>
|
||||
{recentlyClaimedBlock !== null ? (
|
||||
<Cup className='donation-icon' />
|
||||
) : (
|
||||
<Heart className='donation-icon' />
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
const progressDonationText = (
|
||||
<div className='text-center progress-modal-text'>
|
||||
<div className='donation-icon-container'>
|
||||
<Heart className='donation-icon' />
|
||||
</div>
|
||||
<Row>
|
||||
{!closeLabel && (
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
{getDonationText()}
|
||||
{recentlyClaimedBlock !== null && (
|
||||
<b>
|
||||
{t('donate.nicely-done', {
|
||||
block: t(
|
||||
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
|
||||
)
|
||||
})}
|
||||
</b>
|
||||
)}
|
||||
{getCommonDonationText()}
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
@@ -135,7 +133,7 @@ function DonateModal({
|
||||
show={show}
|
||||
>
|
||||
<Modal.Body>
|
||||
{recentlyClaimedBlock ? blockDonationText : progressDonationText}
|
||||
{donationText}
|
||||
<Spacer />
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCompletedPercent } from './completion-modal';
|
||||
import { getCompletedPercentage } from '../../../utils/get-completion-percentage';
|
||||
|
||||
jest.mock('../../../analytics');
|
||||
|
||||
@@ -8,16 +8,16 @@ const completedChallengesIds = ['1', '3', '5'],
|
||||
fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
|
||||
|
||||
describe('<CompletionModal />', () => {
|
||||
describe('getCompletedPercent', () => {
|
||||
describe('getCompletedPercentage', () => {
|
||||
it('returns 25 if one out of four challenges are complete', () => {
|
||||
expect(getCompletedPercent([], currentBlockIds, currentBlockIds[1])).toBe(
|
||||
25
|
||||
);
|
||||
expect(
|
||||
getCompletedPercentage([], currentBlockIds, currentBlockIds[1])
|
||||
).toBe(25);
|
||||
});
|
||||
|
||||
it('returns 75 if three out of four challenges are complete', () => {
|
||||
expect(
|
||||
getCompletedPercent(
|
||||
getCompletedPercentage(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
completedChallengesIds[0]
|
||||
@@ -27,13 +27,13 @@ describe('<CompletionModal />', () => {
|
||||
|
||||
it('returns 100 if all challenges have been completed', () => {
|
||||
expect(
|
||||
getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||
getCompletedPercentage(completedChallengesIds, currentBlockIds, id)
|
||||
).toBe(100);
|
||||
});
|
||||
|
||||
it('returns 100 if more challenges have been complete than exist', () => {
|
||||
expect(
|
||||
getCompletedPercent(fakeCompletedChallengesIds, currentBlockIds, id)
|
||||
getCompletedPercentage(fakeCompletedChallengesIds, currentBlockIds, id)
|
||||
).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,22 +9,23 @@ import { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { dasherize } from '../../../../../utils/slugs';
|
||||
import { isFinalProject } from '../../../../utils/challenge-types';
|
||||
import Login from '../../../components/Header/components/Login';
|
||||
import { executeGA, allowBlockDonationRequests } from '../../../redux/actions';
|
||||
import { executeGA } from '../../../redux/actions';
|
||||
import {
|
||||
isSignedInSelector,
|
||||
allChallengesInfoSelector
|
||||
} from '../../../redux/selectors';
|
||||
import { AllChallengesInfo, ChallengeFiles } from '../../../redux/prop-types';
|
||||
|
||||
import { closeModal, submitChallenge } from '../redux/actions';
|
||||
import {
|
||||
completedChallengesIds,
|
||||
completedChallengesIdsSelector,
|
||||
isCompletionModalOpenSelector,
|
||||
successMessageSelector,
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector
|
||||
challengeMetaSelector,
|
||||
completedPercentageSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
currentBlockIdsSelector
|
||||
} from '../redux/selectors';
|
||||
import CompletionModalBody from './completion-modal-body';
|
||||
|
||||
@@ -33,11 +34,14 @@ import './completion-modal.css';
|
||||
const mapStateToProps = createSelector(
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector,
|
||||
completedChallengesIds,
|
||||
completedChallengesIdsSelector,
|
||||
isCompletionModalOpenSelector,
|
||||
isSignedInSelector,
|
||||
allChallengesInfoSelector,
|
||||
successMessageSelector,
|
||||
completedPercentageSelector,
|
||||
completedChallengesInBlockSelector,
|
||||
currentBlockIdsSelector,
|
||||
(
|
||||
challengeFiles: ChallengeFiles,
|
||||
{
|
||||
@@ -49,7 +53,10 @@ const mapStateToProps = createSelector(
|
||||
isOpen: boolean,
|
||||
isSignedIn: boolean,
|
||||
allChallengesInfo: AllChallengesInfo,
|
||||
message: string
|
||||
message: string,
|
||||
completedPercent: number,
|
||||
completedChallengesInBlock: number,
|
||||
currentBlockIds: string[]
|
||||
) => ({
|
||||
challengeFiles,
|
||||
title,
|
||||
@@ -59,7 +66,10 @@ const mapStateToProps = createSelector(
|
||||
isOpen,
|
||||
isSignedIn,
|
||||
allChallengesInfo,
|
||||
message
|
||||
message,
|
||||
completedPercent,
|
||||
completedChallengesInBlock,
|
||||
currentBlockIds
|
||||
})
|
||||
);
|
||||
|
||||
@@ -69,55 +79,18 @@ const mapDispatchToProps = function (dispatch: Dispatch) {
|
||||
submitChallenge: () => {
|
||||
dispatch(submitChallenge());
|
||||
},
|
||||
allowBlockDonationRequests: (block: string) => {
|
||||
dispatch(allowBlockDonationRequests(block));
|
||||
},
|
||||
executeGA
|
||||
};
|
||||
return () => dispatchers;
|
||||
};
|
||||
|
||||
export function getCompletedPercent(
|
||||
completedChallengesIds: string[] = [],
|
||||
currentBlockIds: string[] = [],
|
||||
currentChallengeId: string
|
||||
): number {
|
||||
const completedChallengesInBlock = getCompletedChallengesInBlock(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
currentChallengeId
|
||||
);
|
||||
const completedPercent = Math.round(
|
||||
(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;
|
||||
blockName: string;
|
||||
certification: string;
|
||||
challengeType: number;
|
||||
close: () => void;
|
||||
completedChallengesIds: string[];
|
||||
currentBlockIds?: string[];
|
||||
executeGA: () => void;
|
||||
challengeFiles: ChallengeFiles;
|
||||
id: string;
|
||||
@@ -125,44 +98,40 @@ interface CompletionModalsProps {
|
||||
isSignedIn: boolean;
|
||||
allChallengesInfo: AllChallengesInfo;
|
||||
message: string;
|
||||
completedPercent: number;
|
||||
completedChallengesInBlock: number;
|
||||
currentBlockIds: string[];
|
||||
submitChallenge: () => void;
|
||||
superBlock: string;
|
||||
t: TFunction;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface CompletionModalInnerState {
|
||||
interface CompletionModalState {
|
||||
downloadURL: null | string;
|
||||
completedPercent: number;
|
||||
completedChallengesInBlock: number;
|
||||
}
|
||||
|
||||
class CompletionModalInner extends Component<
|
||||
class CompletionModal extends Component<
|
||||
CompletionModalsProps,
|
||||
CompletionModalInnerState
|
||||
CompletionModalState
|
||||
> {
|
||||
static displayName: string;
|
||||
constructor(props: CompletionModalsProps) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleKeypress = this.handleKeypress.bind(this);
|
||||
|
||||
this.state = {
|
||||
downloadURL: null,
|
||||
completedPercent: 0,
|
||||
completedChallengesInBlock: 0
|
||||
downloadURL: null
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(
|
||||
props: CompletionModalsProps,
|
||||
state: CompletionModalInnerState
|
||||
): CompletionModalInnerState {
|
||||
props: Readonly<CompletionModalsProps>,
|
||||
state: CompletionModalState
|
||||
): CompletionModalState {
|
||||
const { challengeFiles, isOpen } = props;
|
||||
if (!isOpen) {
|
||||
return {
|
||||
downloadURL: null,
|
||||
completedPercent: 0,
|
||||
completedChallengesInBlock: 0
|
||||
downloadURL: null
|
||||
};
|
||||
}
|
||||
const { downloadURL } = state;
|
||||
@@ -187,25 +156,8 @@ class CompletionModalInner extends Component<
|
||||
});
|
||||
newURL = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
|
||||
const completedPercent = isSignedIn
|
||||
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||
: 0;
|
||||
|
||||
let completedChallengesInBlock = 0;
|
||||
if (currentBlockIds) {
|
||||
completedChallengesInBlock = getCompletedChallengesInBlock(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
downloadURL: newURL,
|
||||
completedPercent,
|
||||
completedChallengesInBlock
|
||||
downloadURL: newURL
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,22 +167,7 @@ class CompletionModalInner extends Component<
|
||||
// Since Hotkeys also listens to Ctrl + Enter we have to stop this event
|
||||
// getting to it.
|
||||
e.stopPropagation();
|
||||
this.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(): void {
|
||||
this.props.submitChallenge();
|
||||
this.checkBlockCompletion();
|
||||
}
|
||||
|
||||
// check block completion for donation
|
||||
checkBlockCompletion(): void {
|
||||
if (
|
||||
this.state.completedPercent === 100 &&
|
||||
!this.props.completedChallengesIds.includes(this.props.id)
|
||||
) {
|
||||
this.props.allowBlockDonationRequests(this.props.blockName);
|
||||
this.props.submitChallenge();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,19 +182,20 @@ class CompletionModalInner extends Component<
|
||||
const {
|
||||
block,
|
||||
close,
|
||||
currentBlockIds,
|
||||
id,
|
||||
isOpen,
|
||||
isSignedIn,
|
||||
message,
|
||||
superBlock = '',
|
||||
t,
|
||||
title
|
||||
title,
|
||||
completedPercent,
|
||||
completedChallengesInBlock,
|
||||
currentBlockIds,
|
||||
submitChallenge
|
||||
} = this.props;
|
||||
|
||||
const { completedPercent, completedChallengesInBlock } = this.state;
|
||||
|
||||
const totalChallengesInBlock = currentBlockIds?.length ?? 0;
|
||||
const totalChallengesInBlock = currentBlockIds.length;
|
||||
|
||||
if (isOpen) {
|
||||
executeGA({ event: 'pageview', pagePath: '/completion-modal' });
|
||||
@@ -303,7 +241,7 @@ class CompletionModalInner extends Component<
|
||||
block={true}
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
onClick={() => this.handleSubmit()}
|
||||
onClick={() => submitChallenge()}
|
||||
>
|
||||
{isSignedIn ? t('buttons.submit-and-go') : t('buttons.go-to-next')}
|
||||
<span className='hidden-xs'> (Ctrl + Enter)</span>
|
||||
@@ -326,40 +264,6 @@ class CompletionModalInner extends Component<
|
||||
}
|
||||
}
|
||||
|
||||
interface Options {
|
||||
isFinalProjectBlock: boolean;
|
||||
}
|
||||
|
||||
const useCurrentBlockIds = (
|
||||
allChallengesInfo: AllChallengesInfo,
|
||||
block: string,
|
||||
certification: string,
|
||||
options?: Options
|
||||
) => {
|
||||
const { challengeEdges, certificateNodes } = allChallengesInfo;
|
||||
const currentCertificateIds = certificateNodes
|
||||
.filter(
|
||||
node => dasherize(node.challenge.certification) === certification
|
||||
)[0]
|
||||
?.challenge.tests.map(test => test.id);
|
||||
const currentBlockIds = challengeEdges
|
||||
.filter(edge => edge.node.challenge.block === block)
|
||||
.map(edge => edge.node.challenge.id);
|
||||
|
||||
return options?.isFinalProjectBlock ? currentCertificateIds : currentBlockIds;
|
||||
};
|
||||
|
||||
const CompletionModal = (props: CompletionModalsProps) => {
|
||||
const currentBlockIds = useCurrentBlockIds(
|
||||
props.allChallengesInfo,
|
||||
props.block || '',
|
||||
props.certification || '',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
{ isFinalProjectBlock: isFinalProject(props.challengeType) }
|
||||
);
|
||||
return <CompletionModalInner currentBlockIds={currentBlockIds} {...props} />;
|
||||
};
|
||||
|
||||
CompletionModal.displayName = 'CompletionModal';
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -2,11 +2,19 @@ import { navigate } from 'gatsby';
|
||||
import { omit } from 'lodash-es';
|
||||
import { ofType } from 'redux-observable';
|
||||
import { empty, of } from 'rxjs';
|
||||
import { catchError, concat, retry, switchMap, tap } from 'rxjs/operators';
|
||||
import {
|
||||
catchError,
|
||||
concat,
|
||||
retry,
|
||||
switchMap,
|
||||
tap,
|
||||
mergeMap
|
||||
} from 'rxjs/operators';
|
||||
import { isChallenge } from '../../../utils/path-parsers';
|
||||
import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
|
||||
import { actionTypes as submitActionTypes } from '../../../redux/action-types';
|
||||
import {
|
||||
allowBlockDonationRequests,
|
||||
submitComplete,
|
||||
updateComplete,
|
||||
updateFailed
|
||||
@@ -25,7 +33,8 @@ import {
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector,
|
||||
challengeTestsSelector,
|
||||
projectFormValuesSelector
|
||||
projectFormValuesSelector,
|
||||
isBlockNewlyCompletedSelector
|
||||
} from './selectors';
|
||||
|
||||
function postChallenge(update, username) {
|
||||
@@ -156,8 +165,9 @@ export default function completionEpic(action$, state$) {
|
||||
ofType(actionTypes.submitChallenge),
|
||||
switchMap(({ type }) => {
|
||||
const state = state$.value;
|
||||
const meta = challengeMetaSelector(state);
|
||||
const { nextChallengePath, challengeType, superBlock } = meta;
|
||||
|
||||
const { nextChallengePath, challengeType, superBlock, block } =
|
||||
challengeMetaSelector(state);
|
||||
|
||||
let submitter = () => of({ type: 'no-user-signed-in' });
|
||||
if (
|
||||
@@ -169,6 +179,7 @@ export default function completionEpic(action$, state$) {
|
||||
challengeType
|
||||
);
|
||||
}
|
||||
|
||||
if (isSignedInSelector(state)) {
|
||||
submitter = submitters[submitTypes[challengeType]];
|
||||
}
|
||||
@@ -177,8 +188,17 @@ export default function completionEpic(action$, state$) {
|
||||
return findPathToNavigateTo(nextChallengePath, superBlock);
|
||||
};
|
||||
|
||||
const canAllowDonationRequest = (state, action) =>
|
||||
isBlockNewlyCompletedSelector(state) &&
|
||||
action.type === submitActionTypes.submitComplete;
|
||||
|
||||
return submitter(type, state).pipe(
|
||||
concat(of(setIsAdvancing(isChallenge(pathToNavigateTo())))),
|
||||
mergeMap(x =>
|
||||
canAllowDonationRequest(state, x)
|
||||
? of(x, allowBlockDonationRequests({ superBlock, block }))
|
||||
: of(x)
|
||||
),
|
||||
tap(res => {
|
||||
if (res.type !== submitActionTypes.updateFailed) {
|
||||
navigate(pathToNavigateTo());
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { challengeTypes } from '../../../../utils/challenge-types';
|
||||
import { completedChallengesSelector } from '../../../redux/selectors';
|
||||
import {
|
||||
completedChallengesSelector,
|
||||
allChallengesInfoSelector,
|
||||
isSignedInSelector
|
||||
} from '../../../redux/selectors';
|
||||
import {
|
||||
getCurrentBlockIds,
|
||||
getCompletedChallengesInBlock,
|
||||
getCompletedPercentage
|
||||
} from '../../../utils/get-completion-percentage';
|
||||
import { ns } from './action-types';
|
||||
|
||||
export const challengeFilesSelector = state => state[ns].challengeFiles;
|
||||
export const challengeMetaSelector = state => state[ns].challengeMeta;
|
||||
export const challengeTestsSelector = state => state[ns].challengeTests;
|
||||
export const consoleOutputSelector = state => state[ns].consoleOut;
|
||||
export const completedChallengesIds = state =>
|
||||
export const completedChallengesIdsSelector = state =>
|
||||
completedChallengesSelector(state).map(node => node.id);
|
||||
export const isChallengeCompletedSelector = state => {
|
||||
const completedChallenges = completedChallengesSelector(state);
|
||||
@@ -82,6 +91,53 @@ export const challengeDataSelector = state => {
|
||||
return challengeData;
|
||||
};
|
||||
|
||||
export const currentBlockIdsSelector = state => {
|
||||
const { block, certification, challengeType } = challengeMetaSelector(state);
|
||||
const allChallengesInfo = allChallengesInfoSelector(state);
|
||||
const currentBlockIds = getCurrentBlockIds(
|
||||
allChallengesInfo,
|
||||
block,
|
||||
certification,
|
||||
challengeType
|
||||
);
|
||||
|
||||
return currentBlockIds;
|
||||
};
|
||||
|
||||
export const completedChallengesInBlockSelector = state => {
|
||||
const completedChallengesIds = completedChallengesIdsSelector(state);
|
||||
const currentBlockIds = currentBlockIdsSelector(state);
|
||||
const { id } = challengeMetaSelector(state);
|
||||
const completedChallengesInBlock = getCompletedChallengesInBlock(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
id
|
||||
);
|
||||
return completedChallengesInBlock;
|
||||
};
|
||||
|
||||
export const completedPercentageSelector = state => {
|
||||
const isSignedIn = isSignedInSelector(state);
|
||||
if (isSignedIn) {
|
||||
const completedChallengesIds = completedChallengesIdsSelector(state);
|
||||
const { id } = challengeMetaSelector(state);
|
||||
const currentBlockIds = currentBlockIdsSelector(state);
|
||||
const completedPercentage = getCompletedPercentage(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
id
|
||||
);
|
||||
return completedPercentage;
|
||||
} else return 0;
|
||||
};
|
||||
|
||||
export const isBlockNewlyCompletedSelector = state => {
|
||||
const completedPercentage = completedPercentageSelector(state);
|
||||
const completedChallengesIds = completedChallengesIdsSelector(state);
|
||||
const { id } = challengeMetaSelector(state);
|
||||
return completedPercentage === 100 && !completedChallengesIds.includes(id);
|
||||
};
|
||||
|
||||
export const attemptsSelector = state => state[ns].attempts;
|
||||
export const canFocusEditorSelector = state => state[ns].canFocusEditor;
|
||||
export const visibleEditorsSelector = state => state[ns].visibleEditors;
|
||||
|
||||
56
client/src/utils/get-completion-percentage.ts
Normal file
56
client/src/utils/get-completion-percentage.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { AllChallengesInfo } from '../redux/prop-types';
|
||||
import { dasherize } from '../../../utils/slugs';
|
||||
import { isFinalProject } from '../../utils/challenge-types';
|
||||
|
||||
export function getCompletedPercentage(
|
||||
completedChallengesIds: string[] = [],
|
||||
currentBlockIds: string[] = [],
|
||||
currentChallengeId: string
|
||||
): number {
|
||||
const completedChallengesInBlock = getCompletedChallengesInBlock(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
currentChallengeId
|
||||
);
|
||||
const completedPercent = Math.round(
|
||||
(completedChallengesInBlock / currentBlockIds.length) * 100
|
||||
);
|
||||
|
||||
return completedPercent > 100 ? 100 : completedPercent;
|
||||
}
|
||||
|
||||
export function getCompletedChallengesInBlock(
|
||||
completedChallengesIds: string[],
|
||||
currentBlockIds: string[],
|
||||
currentChallengeId: string
|
||||
) {
|
||||
const oldCompletionCount = completedChallengesIds.filter(challengeId =>
|
||||
currentBlockIds.includes(challengeId)
|
||||
).length;
|
||||
|
||||
const isAlreadyCompleted =
|
||||
completedChallengesIds.includes(currentChallengeId);
|
||||
|
||||
return isAlreadyCompleted ? oldCompletionCount : oldCompletionCount + 1;
|
||||
}
|
||||
|
||||
export const getCurrentBlockIds = (
|
||||
allChallengesInfo: AllChallengesInfo,
|
||||
block: string,
|
||||
certification: string,
|
||||
challengeType: number
|
||||
) => {
|
||||
const { challengeEdges, certificateNodes } = allChallengesInfo;
|
||||
const currentCertificateIds = certificateNodes
|
||||
.filter(
|
||||
node => dasherize(node.challenge.certification) === certification
|
||||
)[0]
|
||||
?.challenge.tests.map(test => test.id);
|
||||
const currentBlockIds = challengeEdges
|
||||
.filter(edge => edge.node.challenge.block === block)
|
||||
.map(edge => edge.node.challenge.id);
|
||||
|
||||
return isFinalProject(challengeType)
|
||||
? currentCertificateIds
|
||||
: currentBlockIds;
|
||||
};
|
||||
Reference in New Issue
Block a user