From 8b43b30b95f99aed9015da0cd32fb6a99143fa6e Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Wed, 25 Feb 2026 13:25:33 +0200 Subject: [PATCH] fix(client): add message about accepting ahp (#65675) Co-authored-by: Oliver Eyton-Williams --- client/i18n/locales/english/translations.json | 3 +- .../Challenges/exam-download/show.tsx | 160 +++++++++++++----- 2 files changed, 118 insertions(+), 45 deletions(-) diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index e540fcc96c0..36e2d1414d2 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -699,7 +699,8 @@ "exit-header": "Exit Exam", "exit": "Are you sure you want to leave the exam? You will lose any progress you have made.", "exit-yes": "Yes, I want to leave the exam", - "exit-no": "No, I would like to continue the exam" + "exit-no": "No, I would like to continue the exam", + "not-honest": "You need to <0>accept the Academic Honesty Policy to take this exam" }, "ms": { "link-header": "Link your Microsoft account", diff --git a/client/src/templates/Challenges/exam-download/show.tsx b/client/src/templates/Challenges/exam-download/show.tsx index 0e4ab46a7bb..738bce6eb19 100644 --- a/client/src/templates/Challenges/exam-download/show.tsx +++ b/client/src/templates/Challenges/exam-download/show.tsx @@ -11,14 +11,14 @@ import { Row, Col } from '@freecodecamp/ui'; -import { useTranslation, withTranslation } from 'react-i18next'; +import { Trans, useTranslation, withTranslation } from 'react-i18next'; import { createSelector } from 'reselect'; import { connect } from 'react-redux'; import LearnLayout from '../../../components/layouts/learn'; import ChallengeTitle from '../components/challenge-title'; import useDetectOS, { type UserOSState } from '../utils/use-detect-os'; -import { +import type { ChallengeNode, CompletedChallenge, User @@ -36,6 +36,8 @@ import { Attempts } from './attempts'; import ExamTokenControls from './exam-token-controls'; import './show.css'; +import { Link, Loader } from '../../../components/helpers'; +import { SuperBlocks } from '@freecodecamp/shared/config/curriculum'; const { deploymentEnv } = envData; @@ -49,6 +51,110 @@ interface GitProps { prerelease: boolean; } +function PrerequisitesCallout({ + id, + completedChallenges, + challenges, + examSuperBlock, + isSignedIn, + isHonest +}: ExamPrerequisitesProps & { + isSignedIn: boolean; + isHonest: boolean; +}) { + const { t } = useTranslation(); + if (!isSignedIn) { + return null; + } + + if (!isHonest) { + return ( + +

+ + settings + +

+
+ ); + } + + return ( + + ); +} + +interface ExamPrerequisitesProps { + id: string; + completedChallenges: CompletedChallenge[]; + challenges: ChallengeNode['challenge'][]; + examSuperBlock: SuperBlocks; +} + +function ExamPrerequisites({ + id, + completedChallenges, + challenges, + examSuperBlock +}: ExamPrerequisitesProps) { + const { t } = useTranslation(); + const getExamsQuery = examAttempts.useGetExamsQuery(); + const examIdsQuery = examAttempts.useGetExamIdsByChallengeIdQuery(id); + + if (getExamsQuery.isFetching || examIdsQuery.isFetching) { + return ; + } + + if (getExamsQuery.isError || examIdsQuery.isError) { + console.error(getExamsQuery.error); + console.error(examIdsQuery.error); + return null; + } + + if (!getExamsQuery.isSuccess || !examIdsQuery.isSuccess) { + return null; + } + + const examId = examIdsQuery.data.at(0)?.examId; + const exam = getExamsQuery.data.find(examItem => examItem.id === examId); + + if (!exam) { + // This should never happen + return null; + } + + const unmetPrerequisites = exam.prerequisites.filter( + prereq => !completedChallenges.some(challenge => challenge.id === prereq) + ); + const unmetChallenges = challenges.filter( + challenge => + unmetPrerequisites?.includes(challenge.id) && + challenge.superBlock === examSuperBlock + ); + const missingPrerequisites = unmetChallenges.map(challenge => { + return { + id: challenge.id, + title: challenge.title, + slug: challenge.fields?.slug || '' + }; + }); + + if (missingPrerequisites.length < 1) { + return ( + +

{t('learn.exam.qualified')}

+
+ ); + } + + return ; +} + const mapStateToProps = createSelector( completedChallengesSelector, isChallengeCompletedSelector, @@ -168,13 +274,6 @@ function ShowExamDownload({ const [downloadLink, setDownloadLink] = useState(''); const [downloadLinks, setDownloadLinks] = useState([]); - const getExamsQuery = examAttempts.useGetExamsQuery(undefined, { - skip: !isSignedIn - }); - const examIdsQuery = examAttempts.useGetExamIdsByChallengeIdQuery(id, { - skip: !isSignedIn - }); - const userOSState = useDetectOS(); const { t } = useTranslation(); @@ -231,27 +330,6 @@ function ShowExamDownload({ } }, [userOSState]); - const examId = examIdsQuery.data?.at(0)?.examId; - const exam = getExamsQuery.data?.find(examItem => examItem.id === examId); - const unmetPrerequisites = exam?.prerequisites?.filter( - prereq => !completedChallenges.some(challenge => challenge.id === prereq) - ); - const challenges = nodes.filter( - ({ challenge }) => - unmetPrerequisites?.includes(challenge.id) && - challenge.superBlock === examSuperBlock - ); - const missingPrerequisites = challenges.map(({ challenge }) => { - return { - id: challenge.id, - title: challenge.title, - slug: challenge.fields?.slug || '' - }; - }); - - const showPrereqAlert = - isSignedIn && !examIdsQuery.isLoading && !getExamsQuery.isLoading; - return ( @@ -270,20 +348,14 @@ function ShowExamDownload({ {title} - {showPrereqAlert && - (missingPrerequisites.length > 0 ? ( - - ) : ( - -

{t('learn.exam.qualified')}

-
- ))} + challenge)} + completedChallenges={completedChallenges} + examSuperBlock={examSuperBlock} + />

{t('exam.download-header')}

{t('exam.explanation')}