diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx
index 04d5f2e4efb..f87e0bb17ad 100644
--- a/client/src/components/Donation/donation-modal.tsx
+++ b/client/src/components/Donation/donation-modal.tsx
@@ -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 = (
-
-
-
- {!closeLabel && (
-
- {t('donate.nicely-done', { block: recentlyClaimedBlock })}
-
- {getDonationText()}
-
+ {recentlyClaimedBlock !== null ? (
+
+ ) : (
+
)}
-
-
- );
-
- const progressDonationText = (
-
-
-
{!closeLabel && (
- {getDonationText()}
+ {recentlyClaimedBlock !== null && (
+
+ {t('donate.nicely-done', {
+ block: t(
+ `intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
+ )
+ })}
+
+ )}
+ {getCommonDonationText()}
)}
@@ -135,7 +133,7 @@ function DonateModal({
show={show}
>
- {recentlyClaimedBlock ? blockDonationText : progressDonationText}
+ {donationText}
diff --git a/client/src/templates/Challenges/components/completion-modal.test.tsx b/client/src/templates/Challenges/components/completion-modal.test.tsx
index 19aeb5efdcc..7d151bd0588 100644
--- a/client/src/templates/Challenges/components/completion-modal.test.tsx
+++ b/client/src/templates/Challenges/components/completion-modal.test.tsx
@@ -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('', () => {
- 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('', () => {
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);
});
});
diff --git a/client/src/templates/Challenges/components/completion-modal.tsx b/client/src/templates/Challenges/components/completion-modal.tsx
index de8c7076b8c..dcf5ae24650 100644
--- a/client/src/templates/Challenges/components/completion-modal.tsx
+++ b/client/src/templates/Challenges/components/completion-modal.tsx
@@ -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,
+ 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')}
(Ctrl + Enter)
@@ -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 ;
-};
-
CompletionModal.displayName = 'CompletionModal';
export default connect(
diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js
index fb01406a690..c4dc53c0517 100644
--- a/client/src/templates/Challenges/redux/completion-epic.js
+++ b/client/src/templates/Challenges/redux/completion-epic.js
@@ -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());
diff --git a/client/src/templates/Challenges/redux/selectors.js b/client/src/templates/Challenges/redux/selectors.js
index 251aca75638..8515685af8e 100644
--- a/client/src/templates/Challenges/redux/selectors.js
+++ b/client/src/templates/Challenges/redux/selectors.js
@@ -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;
diff --git a/client/src/utils/get-completion-percentage.ts b/client/src/utils/get-completion-percentage.ts
new file mode 100644
index 00000000000..1371604d2d4
--- /dev/null
+++ b/client/src/utils/get-completion-percentage.ts
@@ -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;
+};