diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index faadeed5b05..538c89d1067 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -96,6 +96,8 @@
"exit": "Exit",
"finish-exam": "Finish the exam",
"finish": "Finish",
+ "exit-quiz": "Exit the quiz",
+ "finish-quiz": "Finish the quiz",
"submit-exam-results": "Submit my results",
"verify-trophy": "Verify Trophy",
"link-account": "Link Account",
@@ -519,7 +521,15 @@
"correct-answer": "Correct!",
"incorrect-answer": "Incorrect.",
"unanswered-questions": "The following questions are unanswered: {{ unansweredQuestions }}. You must answer all questions.",
- "have-n-correct-questions": "You have {{ correctAnswerCount }} out of {{ total }} questions correct."
+ "have-n-correct-questions": "You have {{ correctAnswerCount }} out of {{ total }} questions correct.",
+ "finish-modal-header": "Finish Quiz",
+ "finish-modal-body": "Are you sure you want to finish the quiz? You will not be able to change any answers.",
+ "finish-modal-yes": "Yes, I am finished",
+ "finish-modal-no": "No, I would like to continue the quiz",
+ "exit-modal-header": "Exit Quiz",
+ "exit-modal-body": "Are you sure you want to leave the quiz? You will lose any progress you have made.",
+ "exit-modal-yes": "Yes, I want to leave the quiz",
+ "exit-modal-no": "No, I would like to continue the quiz"
},
"exam": {
"qualified": "Congratulations, you have completed all the requirements to qualify for the exam.",
diff --git a/client/package.json b/client/package.json
index 1dc7b98d3f0..226eb94738f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -50,7 +50,7 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@freecodecamp/loop-protect": "3.0.0",
"@freecodecamp/react-calendar-heatmap": "1.1.0",
- "@freecodecamp/ui": "2.1.0",
+ "@freecodecamp/ui": "3.0.0",
"@growthbook/growthbook-react": "0.20.0",
"@loadable/component": "5.16.3",
"@reach/router": "1.3.4",
diff --git a/client/src/templates/Challenges/hooks/index.ts b/client/src/templates/Challenges/hooks/index.ts
new file mode 100644
index 00000000000..c2fd2e19ad1
--- /dev/null
+++ b/client/src/templates/Challenges/hooks/index.ts
@@ -0,0 +1 @@
+export { usePageLeave } from './use-page-leave';
diff --git a/client/src/templates/Challenges/hooks/use-page-leave.ts b/client/src/templates/Challenges/hooks/use-page-leave.ts
new file mode 100644
index 00000000000..4a0ddec8e8e
--- /dev/null
+++ b/client/src/templates/Challenges/hooks/use-page-leave.ts
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { useLocation, globalHistory } from '@reach/router';
+
+interface Props {
+ onWindowClose: (event: BeforeUnloadEvent) => void;
+ onHistoryChange: () => void;
+}
+
+export const usePageLeave = ({ onWindowClose, onHistoryChange }: Props) => {
+ const curLocation = useLocation();
+
+ useEffect(() => {
+ window.addEventListener('beforeunload', onWindowClose);
+
+ // This is a workaround as @reach/router doesn't support blocking history change.
+ // https://github.com/reach/router/issues/464
+ const unlistenHistory = globalHistory.listen(({ action, location }) => {
+ const isBack = action === 'POP';
+ const isRouteChanged =
+ action === 'PUSH' && location.pathname !== curLocation.pathname;
+
+ if (isBack || isRouteChanged) {
+ onHistoryChange();
+ }
+ });
+
+ return () => {
+ window.removeEventListener('beforeunload', onWindowClose);
+ unlistenHistory();
+ };
+ }, [onWindowClose, onHistoryChange, curLocation]);
+};
diff --git a/client/src/templates/Challenges/quiz/exit-quiz-modal.tsx b/client/src/templates/Challenges/quiz/exit-quiz-modal.tsx
new file mode 100644
index 00000000000..c2bf4877811
--- /dev/null
+++ b/client/src/templates/Challenges/quiz/exit-quiz-modal.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { Button, Modal, Spacer } from '@freecodecamp/ui';
+
+import { closeModal } from '../redux/actions';
+import { isExitQuizModalOpenSelector } from '../redux/selectors';
+
+interface ExitQuizModalProps {
+ closeExitQuizModal: () => void;
+ isExitQuizModalOpen: boolean;
+ onExit: () => void;
+}
+
+const mapStateToProps = (state: unknown) => ({
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
+ isExitQuizModalOpen: isExitQuizModalOpenSelector(state)
+});
+
+const mapDispatchToProps = {
+ closeExitQuizModal: () => closeModal('exitQuiz')
+};
+
+const ExitQuizModal = ({
+ closeExitQuizModal,
+ isExitQuizModalOpen,
+ onExit
+}: ExitQuizModalProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('learn.quiz.exit-modal-header')}
+
+
+ {t('learn.quiz.exit-modal-body')}
+
+
+
+
+
+
+
+ );
+};
+
+ExitQuizModal.displayName = 'ExitQuizModal';
+
+export default connect(mapStateToProps, mapDispatchToProps)(ExitQuizModal);
diff --git a/client/src/templates/Challenges/quiz/finish-quiz-modal.tsx b/client/src/templates/Challenges/quiz/finish-quiz-modal.tsx
new file mode 100644
index 00000000000..b0afb1cd043
--- /dev/null
+++ b/client/src/templates/Challenges/quiz/finish-quiz-modal.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { Button, Modal, Spacer } from '@freecodecamp/ui';
+
+import { closeModal } from '../redux/actions';
+import { isFinishQuizModalOpenSelector } from '../redux/selectors';
+
+interface FinishQuizModalProps {
+ closeFinishQuizModal: () => void;
+ isFinishQuizModalOpen: boolean;
+ onFinish: () => void;
+}
+
+const mapStateToProps = (state: unknown) => ({
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
+ isFinishQuizModalOpen: isFinishQuizModalOpenSelector(state)
+});
+
+const mapDispatchToProps = {
+ closeFinishQuizModal: () => closeModal('finishQuiz')
+};
+
+const FinishQuizModal = ({
+ closeFinishQuizModal,
+ isFinishQuizModalOpen,
+ onFinish
+}: FinishQuizModalProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('learn.quiz.finish-modal-header')}
+
+
+ {t('learn.quiz.finish-modal-body')}
+
+
+
+
+
+
+
+ );
+};
+
+FinishQuizModal.displayName = 'FinishQuizModal';
+
+export default connect(mapStateToProps, mapDispatchToProps)(FinishQuizModal);
diff --git a/client/src/templates/Challenges/quiz/show.tsx b/client/src/templates/Challenges/quiz/show.tsx
index 7c828a49e5a..29d61963c53 100644
--- a/client/src/templates/Challenges/quiz/show.tsx
+++ b/client/src/templates/Challenges/quiz/show.tsx
@@ -1,5 +1,5 @@
-import { graphql } from 'gatsby';
-import React, { useEffect, useRef, useState } from 'react';
+import { graphql, navigate } from 'gatsby';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
import Helmet from 'react-helmet';
import { ObserveKeys } from 'react-hotkeys';
import { useTranslation } from 'react-i18next';
@@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
+import { useLocation } from '@reach/router';
import {
Container,
Col,
@@ -21,7 +22,6 @@ import {
import { shuffleArray } from '../../../../../shared/utils/shuffle-array';
import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
-// import { challengeTypes } from '../../../../../shared/config/challenge-types';
import ChallengeDescription from '../components/challenge-description';
import Hotkeys from '../components/hotkeys';
import ChallengeTitle from '../components/challenge-title';
@@ -30,11 +30,15 @@ import {
challengeMounted,
updateChallengeMeta,
openModal,
+ closeModal,
updateSolutionFormValues,
initTests
} from '../redux/actions';
import { isChallengeCompletedSelector } from '../redux/selectors';
import PrismFormatted from '../components/prism-formatted';
+import { usePageLeave } from '../hooks';
+import ExitQuizModal from './exit-quiz-modal';
+import FinishQuizModal from './finish-quiz-modal';
import './show.css';
@@ -52,7 +56,11 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
updateChallengeMeta,
challengeMounted,
updateSolutionFormValues,
- openCompletionModal: () => openModal('completion')
+ openCompletionModal: () => openModal('completion'),
+ openExitQuizModal: () => openModal('exitQuiz'),
+ closeExitQuizModal: () => closeModal('exitQuiz'),
+ openFinishQuizModal: () => openModal('finishQuiz'),
+ closeFinishQuizModal: () => closeModal('finishQuiz')
},
dispatch
);
@@ -64,12 +72,16 @@ interface ShowQuizProps {
description: string;
initTests: (xs: Test[]) => void;
isChallengeCompleted: boolean;
- openCompletionModal: () => void;
pageContext: {
challengeMeta: ChallengeMeta;
};
updateChallengeMeta: (arg0: ChallengeMeta) => void;
updateSolutionFormValues: () => void;
+ openCompletionModal: () => void;
+ openExitQuizModal: () => void;
+ closeExitQuizModal: () => void;
+ openFinishQuizModal: () => void;
+ closeFinishQuizModal: () => void;
}
const ShowQuiz = ({
@@ -77,7 +89,7 @@ const ShowQuiz = ({
data: {
challengeNode: {
challenge: {
- fields: { tests },
+ fields: { tests, blockHashSlug },
title,
description,
challengeType,
@@ -92,10 +104,16 @@ const ShowQuiz = ({
pageContext: { challengeMeta },
initTests,
updateChallengeMeta,
+ isChallengeCompleted,
openCompletionModal,
- isChallengeCompleted
+ openExitQuizModal,
+ closeExitQuizModal,
+ openFinishQuizModal,
+ closeFinishQuizModal
}: ShowQuizProps) => {
const { t } = useTranslation();
+ const curLocation = useLocation();
+
const { nextChallengePath, prevChallengePath } = challengeMeta;
const container = useRef(null);
@@ -106,6 +124,10 @@ const ShowQuiz = ({
// `isPassed` is used as a flag to conditionally render the test or submit button.
const [isPassed, setIsPassed] = useState(false);
+ const [showUnanswered, setShowUnanswered] = useState(false);
+
+ const [exitConfirmed, setExitConfirmed] = useState(false);
+
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
@@ -146,6 +168,7 @@ const ShowQuiz = ({
const {
questions: quizData,
validateAnswers,
+ validated,
correctAnswerCount
} = useQuiz({
initialQuestions: initialQuizData,
@@ -153,13 +176,18 @@ const ShowQuiz = ({
correct: t('learn.quiz.correct-answer'),
incorrect: t('learn.quiz.incorrect-answer')
},
+ passingGrade: 85,
onSuccess: () => {
- openCompletionModal();
- setIsPassed(true);
+ openCompletionModal(), setIsPassed(true);
},
onFailure: () => setIsPassed(false)
});
+ const unanswered = quizData.reduce(
+ (acc, curr, id) => (curr.selectedAnswer == null ? [...acc, id + 1] : acc),
+ []
+ );
+
useEffect(() => {
initTests(tests);
updateChallengeMeta({
@@ -191,40 +219,85 @@ const ShowQuiz = ({
updateChallengeMeta
]);
- const handleAnswersCheck = () => {
+ const handleFinishQuiz = () => {
+ setShowUnanswered(true);
+
+ if (unanswered.length === 0) {
+ openFinishQuizModal();
+ }
+ };
+
+ const handleFinishQuizModalBtnClick = () => {
validateAnswers();
setHasSubmitted(true);
+ closeFinishQuizModal();
};
const handleSubmitAndGo = () => {
openCompletionModal();
};
+ const handleExitQuiz = () => {
+ openExitQuizModal();
+ };
+
+ const handleExitQuizModalBtnClick = () => {
+ setExitConfirmed(true);
+ void navigate(blockHashSlug);
+ closeExitQuizModal();
+ };
+
+ const onWindowClose = useCallback(
+ (event: BeforeUnloadEvent) => {
+ event.preventDefault();
+ window.confirm(t('misc.navigation-warning'));
+ },
+ [t]
+ );
+
+ const onHistoryChange = useCallback(() => {
+ // We don't block navigation in the following cases.
+ // - When campers have submitted the quiz:
+ // - If they don't pass, the Finish Quiz button is disabled, there isn't anything for them to do other than leaving the page
+ // - If they pass, the Submit-and-go button shows up, and campers should be allowed to leave the page
+ // - When they have clicked the exit button on the exit modal
+ if (hasSubmitted || exitConfirmed) {
+ return;
+ }
+
+ void navigate(`${curLocation.pathname}`);
+ openExitQuizModal();
+ }, [curLocation.pathname, hasSubmitted, exitConfirmed, openExitQuizModal]);
+
+ usePageLeave({
+ onWindowClose,
+ onHistoryChange
+ });
+
function getErrorMessage() {
- if (!hasSubmitted) return '';
-
- const unansweredList = quizData.reduce(
- (acc, curr, id) => (curr.selectedAnswer == null ? [...acc, id + 1] : acc),
- []
- );
-
- if (unansweredList.length > 0) {
+ if (showUnanswered && unanswered.length > 0) {
return t('learn.quiz.unanswered-questions', {
- unansweredQuestions: unansweredList.join(', ')
+ unansweredQuestions: unanswered.join(', ')
});
}
- return t('learn.quiz.have-n-correct-questions', {
- correctAnswerCount,
- total: quiz.length
- });
+ if (validated) {
+ // TODO: Update the message to include link(s) to the review materials
+ // if campers didn't pass the quiz.
+ return t('learn.quiz.have-n-correct-questions', {
+ correctAnswerCount,
+ total: quiz.length
+ });
+ }
+
+ return '';
}
const errorMessage = getErrorMessage();
return (
- {/*
- There are three cases for the button display:
- 1. Campers submit the answers but don't pass
- 2. Campers submit the answers and pass, click the submit button on the completion modal
- 3. Campers submit the answers and pass, but they close the completion modal
-
- This rendering logic is only handling (2) and (3).
- TODO: Update the logic to handle (1).
- The code should render a link that points campers to the module's review block.
- */}
{!isPassed ? (
-
+ <>
+
+ >
) : (
);
@@ -307,6 +379,7 @@ export const query = graphql`
superBlock
block
fields {
+ blockHashSlug
blockName
slug
tests {
diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js
index 2dbf477d233..0e331c87491 100644
--- a/client/src/templates/Challenges/redux/index.js
+++ b/client/src/templates/Challenges/redux/index.js
@@ -43,6 +43,8 @@ const initialState = {
reset: false,
exitExam: false,
finishExam: false,
+ exitQuiz: false,
+ finishQuiz: false,
examResults: false,
survey: false,
projectPreview: false,
diff --git a/client/src/templates/Challenges/redux/selectors.js b/client/src/templates/Challenges/redux/selectors.js
index cab9c97eabc..ea893c2ad9e 100644
--- a/client/src/templates/Challenges/redux/selectors.js
+++ b/client/src/templates/Challenges/redux/selectors.js
@@ -37,6 +37,9 @@ export const isFinishExamModalOpenSelector = state =>
export const isSurveyModalOpenSelector = state => state[ns].modal.survey;
export const isExamResultsModalOpenSelector = state =>
state[ns].modal.examResults;
+export const isExitQuizModalOpenSelector = state => state[ns].modal.exitQuiz;
+export const isFinishQuizModalOpenSelector = state =>
+ state[ns].modal.finishQuiz;
export const isProjectPreviewModalOpenSelector = state =>
state[ns].modal.projectPreview;
export const isShortcutsModalOpenSelector = state => state[ns].modal.shortcuts;
diff --git a/e2e/quiz-challenge.spec.ts b/e2e/quiz-challenge.spec.ts
new file mode 100644
index 00000000000..fb6509da010
--- /dev/null
+++ b/e2e/quiz-challenge.spec.ts
@@ -0,0 +1,137 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Quiz challenge', () => {
+ test.skip(
+ () => process.env.SHOW_UPCOMING_CHANGES !== 'true',
+ 'The FSD superblock is not available if SHOW_UPCOMING_CHANGES is false'
+ );
+
+ test.use({ storageState: 'playwright/.auth/certified-user.json' });
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto(
+ '/learn/full-stack-developer/quiz-basic-html/quiz-basic-html'
+ );
+ });
+
+ test('should display a list of unanswered questions if user has not answered all questions', async ({
+ page
+ }) => {
+ // Wait for the page content to render
+ await expect(page.getByRole('radiogroup')).toHaveCount(20);
+
+ for (let i = 0; i < 15; i++) {
+ const radioGroups = await page.getByRole('radiogroup').all();
+ await radioGroups[i].getByRole('radio').first().click();
+ }
+
+ await page.getByRole('button', { name: 'Finish the quiz' }).click();
+
+ await expect(
+ page.getByText(
+ 'The following questions are unanswered: 16, 17, 18, 19, 20. You must answer all questions.'
+ )
+ ).toBeVisible();
+ });
+
+ test('should show a confirm finish modal when user clicks the finish button, and disable the quiz once they have confirmed', async ({
+ page
+ }) => {
+ // Wait for the page content to render
+ await expect(page.getByRole('radiogroup')).toHaveCount(20);
+
+ const radioGroups = await page.getByRole('radiogroup').all();
+
+ for (const radioGroup of radioGroups) {
+ await radioGroup.getByRole('radio').first().click();
+ }
+
+ await page.getByRole('button', { name: 'Finish the quiz' }).click();
+ await expect(
+ page.getByRole('dialog', { name: 'Finish Quiz' })
+ ).toBeVisible();
+
+ await page.getByRole('button', { name: 'Yes, I am finished' }).click();
+ await expect(
+ page.getByRole('dialog', { name: 'Finish Quiz' })
+ ).toBeHidden();
+
+ const radios = await page.getByRole('radio').all();
+
+ for (const radio of radios) {
+ await expect(radio).toBeDisabled();
+ }
+ });
+
+ test('should show a confirm exit modal when user clicks on the exit button', async ({
+ page
+ }) => {
+ // Wait for the page content to render
+ await expect(page.getByRole('radiogroup')).toHaveCount(20);
+
+ await page.getByRole('button', { name: 'Exit the quiz' }).click();
+
+ // The navigation should be blocked, the user should stay on the same page
+ await expect(page).toHaveURL(
+ '/learn/front-end-development/quiz-basic-html/quiz-basic-html'
+ );
+
+ await expect(page.getByRole('dialog')).toBeVisible();
+ await expect(page.getByRole('dialog', { name: 'Exit Quiz' })).toBeVisible();
+
+ await page
+ .getByRole('button', { name: 'No, I would like to continue the quiz' })
+ .click();
+ await expect(page.getByRole('dialog', { name: 'Exit Quiz' })).toBeHidden();
+
+ await page.getByRole('button', { name: 'Exit the quiz' }).click();
+ await expect(page.getByRole('dialog', { name: 'Exit Quiz' })).toBeVisible();
+ await page
+ .getByRole('button', { name: 'Yes, I want to leave the quiz' })
+ .click();
+
+ await page.waitForURL('/learn/front-end-development/#quiz-basic-html');
+ await expect(
+ page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
+ ).toBeVisible();
+ });
+
+ test('should show a confirm exit modal when user clicks on a link', async ({
+ page
+ }) => {
+ // Wait for the page content to render
+ await expect(page.getByRole('radiogroup')).toHaveCount(20);
+
+ await page.getByRole('link', { name: 'Basic HTML Quiz' }).click();
+
+ await expect(page.getByRole('dialog', { name: 'Exit Quiz' })).toBeVisible();
+ await page
+ .getByRole('button', { name: 'Yes, I want to leave the quiz' })
+ .click();
+
+ await page.waitForURL('/learn/front-end-development/#quiz-basic-html');
+ await expect(
+ page.getByRole('heading', { level: 3, name: 'Basic HTML Quiz' })
+ ).toBeVisible();
+ });
+
+ test('should show a confirm exit modal when user closes the page', async ({
+ page,
+ browserName
+ }) => {
+ test.skip(
+ browserName === 'webkit' || browserName === 'chromium',
+ 'This test is flaky on Chromium and WebKit'
+ );
+
+ // Wait for the page content to render
+ await expect(page.getByRole('radiogroup')).toHaveCount(20);
+
+ page.on('dialog', async dialog => {
+ expect(dialog.type()).toBe('beforeunload');
+ await dialog.dismiss();
+ });
+
+ await page.close({ runBeforeUnload: true });
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e99602d15d4..e6bd9a5e67e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -471,8 +471,8 @@ importers:
specifier: 1.1.0
version: 1.1.0(react@16.14.0)
'@freecodecamp/ui':
- specifier: 2.1.0
- version: 2.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
+ specifier: 3.0.0
+ version: 3.0.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
'@growthbook/growthbook-react':
specifier: 0.20.0
version: 0.20.0(react@16.14.0)
@@ -3090,8 +3090,8 @@ packages:
peerDependencies:
react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
- '@freecodecamp/ui@2.1.0':
- resolution: {integrity: sha512-KL9QVvBlVjkls9BWTpsxNQdp88OmR+x9qE/jNvKcGRByFVsLXbAblb1cRcz8YXPPdB4vpImMT8Q3baxZTHn28Q==}
+ '@freecodecamp/ui@3.0.0':
+ resolution: {integrity: sha512-/Gqw8YLYgpugIJctyGuqTjN+Tp9zT4o126rbct9bISdgiCH0+qFkbksGoH/B2lVyFzHg8/Pl1i5t2jvuxMT19Q==}
engines: {node: '>=20', pnpm: '9'}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0
@@ -3571,6 +3571,15 @@ packages:
'@types/react':
optional: true
+ '@radix-ui/react-context@1.1.1':
+ resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@radix-ui/react-direction@1.1.0':
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies:
@@ -3589,8 +3598,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-presence@1.1.0':
- resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
+ '@radix-ui/react-presence@1.1.1':
+ resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -3637,8 +3646,8 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-tabs@1.1.0':
- resolution: {integrity: sha512-bZgOKB/LtZIij75FSuPzyEti/XBhJH52ExgtdVqjCIh+Nx/FW+LhnbXtbCzIi34ccyMsyOja8T0thCzoHFXNKA==}
+ '@radix-ui/react-tabs@1.1.1':
+ resolution: {integrity: sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
@@ -16976,13 +16985,13 @@ snapshots:
prop-types: 15.8.1
react: 16.14.0
- '@freecodecamp/ui@2.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
+ '@freecodecamp/ui@3.0.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
dependencies:
'@fortawesome/fontawesome-svg-core': 6.6.0
'@fortawesome/free-solid-svg-icons': 6.6.0
'@fortawesome/react-fontawesome': 0.2.2(@fortawesome/fontawesome-svg-core@6.6.0)(react@16.14.0)
'@headlessui/react': 1.7.19(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
- '@radix-ui/react-tabs': 1.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
+ '@radix-ui/react-tabs': 1.1.1(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
babel-plugin-prismjs: 2.1.0(prismjs@1.29.0)
prismjs: 1.29.0
react: 16.14.0
@@ -17595,6 +17604,12 @@ snapshots:
optionalDependencies:
'@types/react': 16.14.56
+ '@radix-ui/react-context@1.1.1(@types/react@16.14.56)(react@16.14.0)':
+ dependencies:
+ react: 16.14.0
+ optionalDependencies:
+ '@types/react': 16.14.56
+
'@radix-ui/react-direction@1.1.0(@types/react@16.14.56)(react@16.14.0)':
dependencies:
react: 16.14.0
@@ -17608,7 +17623,7 @@ snapshots:
optionalDependencies:
'@types/react': 16.14.56
- '@radix-ui/react-presence@1.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
+ '@radix-ui/react-presence@1.1.1(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@16.14.56)(react@16.14.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@16.14.56)(react@16.14.0)
@@ -17651,13 +17666,13 @@ snapshots:
optionalDependencies:
'@types/react': 16.14.56
- '@radix-ui/react-tabs@1.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
+ '@radix-ui/react-tabs@1.1.1(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)':
dependencies:
'@radix-ui/primitive': 1.1.0
- '@radix-ui/react-context': 1.1.0(@types/react@16.14.56)(react@16.14.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@16.14.56)(react@16.14.0)
'@radix-ui/react-direction': 1.1.0(@types/react@16.14.56)(react@16.14.0)
'@radix-ui/react-id': 1.1.0(@types/react@16.14.56)(react@16.14.0)
- '@radix-ui/react-presence': 1.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
+ '@radix-ui/react-presence': 1.1.1(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
'@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@16.9.24)(@types/react@16.14.56)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@16.14.56)(react@16.14.0)