Skip to content

Commit 2e76ea1

Browse files
feat(client): handle quiz finish and exit (freeCodeCamp#56644)
Co-authored-by: Oliver Eyton-Williams <[email protected]>
1 parent e2cc42c commit 2e76ea1

File tree

11 files changed

+447
-57
lines changed

11 files changed

+447
-57
lines changed

client/i18n/locales/english/translations.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
"exit": "Exit",
9797
"finish-exam": "Finish the exam",
9898
"finish": "Finish",
99+
"exit-quiz": "Exit the quiz",
100+
"finish-quiz": "Finish the quiz",
99101
"submit-exam-results": "Submit my results",
100102
"verify-trophy": "Verify Trophy",
101103
"link-account": "Link Account",
@@ -519,7 +521,15 @@
519521
"correct-answer": "Correct!",
520522
"incorrect-answer": "Incorrect.",
521523
"unanswered-questions": "The following questions are unanswered: {{ unansweredQuestions }}. You must answer all questions.",
522-
"have-n-correct-questions": "You have {{ correctAnswerCount }} out of {{ total }} questions correct."
524+
"have-n-correct-questions": "You have {{ correctAnswerCount }} out of {{ total }} questions correct.",
525+
"finish-modal-header": "Finish Quiz",
526+
"finish-modal-body": "Are you sure you want to finish the quiz? You will not be able to change any answers.",
527+
"finish-modal-yes": "Yes, I am finished",
528+
"finish-modal-no": "No, I would like to continue the quiz",
529+
"exit-modal-header": "Exit Quiz",
530+
"exit-modal-body": "Are you sure you want to leave the quiz? You will lose any progress you have made.",
531+
"exit-modal-yes": "Yes, I want to leave the quiz",
532+
"exit-modal-no": "No, I would like to continue the quiz"
523533
},
524534
"exam": {
525535
"qualified": "Congratulations, you have completed all the requirements to qualify for the exam.",

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@fortawesome/react-fontawesome": "0.2.0",
5151
"@freecodecamp/loop-protect": "3.0.0",
5252
"@freecodecamp/react-calendar-heatmap": "1.1.0",
53-
"@freecodecamp/ui": "2.1.0",
53+
"@freecodecamp/ui": "3.0.0",
5454
"@growthbook/growthbook-react": "0.20.0",
5555
"@loadable/component": "5.16.3",
5656
"@reach/router": "1.3.4",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { usePageLeave } from './use-page-leave';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useEffect } from 'react';
2+
import { useLocation, globalHistory } from '@reach/router';
3+
4+
interface Props {
5+
onWindowClose: (event: BeforeUnloadEvent) => void;
6+
onHistoryChange: () => void;
7+
}
8+
9+
export const usePageLeave = ({ onWindowClose, onHistoryChange }: Props) => {
10+
const curLocation = useLocation();
11+
12+
useEffect(() => {
13+
window.addEventListener('beforeunload', onWindowClose);
14+
15+
// This is a workaround as @reach/router doesn't support blocking history change.
16+
// https://github.com/reach/router/issues/464
17+
const unlistenHistory = globalHistory.listen(({ action, location }) => {
18+
const isBack = action === 'POP';
19+
const isRouteChanged =
20+
action === 'PUSH' && location.pathname !== curLocation.pathname;
21+
22+
if (isBack || isRouteChanged) {
23+
onHistoryChange();
24+
}
25+
});
26+
27+
return () => {
28+
window.removeEventListener('beforeunload', onWindowClose);
29+
unlistenHistory();
30+
};
31+
}, [onWindowClose, onHistoryChange, curLocation]);
32+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import { useTranslation } from 'react-i18next';
4+
import { Button, Modal, Spacer } from '@freecodecamp/ui';
5+
6+
import { closeModal } from '../redux/actions';
7+
import { isExitQuizModalOpenSelector } from '../redux/selectors';
8+
9+
interface ExitQuizModalProps {
10+
closeExitQuizModal: () => void;
11+
isExitQuizModalOpen: boolean;
12+
onExit: () => void;
13+
}
14+
15+
const mapStateToProps = (state: unknown) => ({
16+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
17+
isExitQuizModalOpen: isExitQuizModalOpenSelector(state)
18+
});
19+
20+
const mapDispatchToProps = {
21+
closeExitQuizModal: () => closeModal('exitQuiz')
22+
};
23+
24+
const ExitQuizModal = ({
25+
closeExitQuizModal,
26+
isExitQuizModalOpen,
27+
onExit
28+
}: ExitQuizModalProps) => {
29+
const { t } = useTranslation();
30+
31+
return (
32+
<Modal
33+
onClose={closeExitQuizModal}
34+
open={isExitQuizModalOpen}
35+
variant='danger'
36+
>
37+
<Modal.Header closeButtonClassNames='close'>
38+
{t('learn.quiz.exit-modal-header')}
39+
</Modal.Header>
40+
<Modal.Body alignment='center'>
41+
{t('learn.quiz.exit-modal-body')}
42+
</Modal.Body>
43+
<Modal.Footer>
44+
<Button block variant='primary' onClick={closeExitQuizModal}>
45+
{t('learn.quiz.exit-modal-no')}
46+
</Button>
47+
<Spacer size='xxs' />
48+
<Button block variant='danger' onClick={onExit}>
49+
{t('learn.quiz.exit-modal-yes')}
50+
</Button>
51+
</Modal.Footer>
52+
</Modal>
53+
);
54+
};
55+
56+
ExitQuizModal.displayName = 'ExitQuizModal';
57+
58+
export default connect(mapStateToProps, mapDispatchToProps)(ExitQuizModal);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import { useTranslation } from 'react-i18next';
4+
import { Button, Modal, Spacer } from '@freecodecamp/ui';
5+
6+
import { closeModal } from '../redux/actions';
7+
import { isFinishQuizModalOpenSelector } from '../redux/selectors';
8+
9+
interface FinishQuizModalProps {
10+
closeFinishQuizModal: () => void;
11+
isFinishQuizModalOpen: boolean;
12+
onFinish: () => void;
13+
}
14+
15+
const mapStateToProps = (state: unknown) => ({
16+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
17+
isFinishQuizModalOpen: isFinishQuizModalOpenSelector(state)
18+
});
19+
20+
const mapDispatchToProps = {
21+
closeFinishQuizModal: () => closeModal('finishQuiz')
22+
};
23+
24+
const FinishQuizModal = ({
25+
closeFinishQuizModal,
26+
isFinishQuizModalOpen,
27+
onFinish
28+
}: FinishQuizModalProps) => {
29+
const { t } = useTranslation();
30+
31+
return (
32+
<Modal onClose={closeFinishQuizModal} open={isFinishQuizModalOpen}>
33+
<Modal.Header closeButtonClassNames='close'>
34+
{t('learn.quiz.finish-modal-header')}
35+
</Modal.Header>
36+
<Modal.Body alignment='center'>
37+
{t('learn.quiz.finish-modal-body')}
38+
</Modal.Body>
39+
<Modal.Footer>
40+
<Button block size='medium' variant='primary' onClick={onFinish}>
41+
{t('learn.quiz.finish-modal-yes')}
42+
</Button>
43+
<Spacer size='xxs' />
44+
<Button
45+
block
46+
size='medium'
47+
variant='primary'
48+
onClick={closeFinishQuizModal}
49+
>
50+
{t('learn.quiz.finish-modal-no')}
51+
</Button>
52+
</Modal.Footer>
53+
</Modal>
54+
);
55+
};
56+
57+
FinishQuizModal.displayName = 'FinishQuizModal';
58+
59+
export default connect(mapStateToProps, mapDispatchToProps)(FinishQuizModal);

0 commit comments

Comments
 (0)