fix(client): add message about accepting ahp (#65675)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Shaun Hamilton
2026-02-25 13:25:33 +02:00
committed by GitHub
parent 07d3bd9a74
commit 8b43b30b95
2 changed files with 118 additions and 45 deletions

View File

@@ -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</0> to take this exam"
},
"ms": {
"link-header": "Link your Microsoft account",

View File

@@ -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 (
<Callout variant='caution' label={t('misc.caution')}>
<p>
<Trans i18nKey={'learn.exam.not-honest'}>
<Link to={'/settings#honesty'}>settings</Link>
</Trans>
</p>
</Callout>
);
}
return (
<ExamPrerequisites
id={id}
completedChallenges={completedChallenges}
challenges={challenges}
examSuperBlock={examSuperBlock}
/>
);
}
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 <Loader />;
}
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 (
<Callout className='exam-qualified' variant='note' label={t('misc.note')}>
<p>{t('learn.exam.qualified')}</p>
</Callout>
);
}
return <MissingPrerequisites missingPrerequisites={missingPrerequisites} />;
}
const mapStateToProps = createSelector(
completedChallengesSelector,
isChallengeCompletedSelector,
@@ -168,13 +274,6 @@ function ShowExamDownload({
const [downloadLink, setDownloadLink] = useState<string | undefined>('');
const [downloadLinks, setDownloadLinks] = useState<string[]>([]);
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 (
<LearnLayout>
<Helmet>
@@ -270,20 +348,14 @@ function ShowExamDownload({
{title}
</ChallengeTitle>
<Spacer size='m' />
{showPrereqAlert &&
(missingPrerequisites.length > 0 ? (
<MissingPrerequisites
missingPrerequisites={missingPrerequisites}
/>
) : (
<Callout
className='exam-qualified'
variant='note'
label={t('misc.note')}
>
<p>{t('learn.exam.qualified')}</p>
</Callout>
))}
<PrerequisitesCallout
isSignedIn={isSignedIn}
isHonest={user?.isHonest ?? false}
id={id}
challenges={nodes.map(({ challenge }) => challenge)}
completedChallenges={completedChallenges}
examSuperBlock={examSuperBlock}
/>
<h2>{t('exam.download-header')}</h2>
<p>{t('exam.explanation')}</p>
<Spacer size='l' />